Index: KernelRecordsMVC.Web/Controllers/AccountController.cs
===================================================================
--- KernelRecordsMVC.Web/Controllers/AccountController.cs	(revision 08aefc6f3b51f4bfcb7471a2fd319c064171b1c5)
+++ KernelRecordsMVC.Web/Controllers/AccountController.cs	(revision fa0fbaf2cdd288351eeaf3d4d17d8a6158ea5adc)
@@ -1,9 +1,11 @@
-﻿using Microsoft.AspNetCore.Mvc;
-using KernelRecordsMVC.Data;
+﻿using System.Security.Claims;
+using KernelRecordsMVC.Application.ViewModels;
+using KernelRecordsMVC.Infrastructure.Data;
 using KernelRecordsMVC.Models;
-using KernelRecordsMVC.Application.ViewModels;
-using Microsoft.AspNetCore.Http;
-
-namespace KernelRecordsMVC.Controllers;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+
+namespace KernelRecordsMVC.Web.Controllers;
 
 public class AccountController : Controller
@@ -16,7 +18,8 @@
     }
 
-    // ==============================
+
+    // =========================================================
     // REGISTER - GET
-    // ==============================
+    // =========================================================
 
     [HttpGet]
@@ -27,7 +30,7 @@
 
 
-    // ==============================
+    // =========================================================
     // REGISTER - POST
-    // ==============================
+    // =========================================================
 
     [HttpPost]
@@ -61,6 +64,6 @@
             Username = model.Username,
 
-            // We'll replace this with proper password hashing
-            // in the security cleanup section.
+            // TODO:
+            // Replace with password hashing later.
             Password = model.Password,
 
@@ -74,4 +77,5 @@
         _context.SaveChanges();
 
+
         // Every normal registered user is a Consumer.
         var consumer = new Consumer
@@ -89,7 +93,7 @@
 
 
-    // ==============================
+    // =========================================================
     // LOGIN - GET
-    // ==============================
+    // =========================================================
 
     [HttpGet]
@@ -100,11 +104,12 @@
 
 
-    // ==============================
+    // =========================================================
     // LOGIN - POST
-    // ==============================
+    // =========================================================
 
     [HttpPost]
     [ValidateAntiForgeryToken]
-    public IActionResult Login(LoginViewModel model)
+    public async Task<IActionResult> Login(
+        LoginViewModel model)
     {
         if (!ModelState.IsValid)
@@ -125,4 +130,78 @@
         }
 
+
+        // =====================================================
+        // DETERMINE ACCOUNT TYPE
+        // =====================================================
+
+        var admin = _context.Admins
+            .FirstOrDefault(x => x.UserId == user.UserId);
+
+        string role;
+
+        if (admin != null)
+        {
+            role = "Admin";
+        }
+        else
+        {
+            role = "Consumer";
+        }
+
+
+        // =====================================================
+        // CREATE CLAIMS
+        // =====================================================
+
+        var claims = new List<Claim>
+        {
+            new Claim(
+                ClaimTypes.NameIdentifier,
+                user.UserId.ToString()),
+
+            new Claim(
+                ClaimTypes.Name,
+                user.Username),
+
+            new Claim(
+                ClaimTypes.Email,
+                user.Email),
+
+            new Claim(
+                ClaimTypes.Role,
+                role)
+        };
+
+
+        if (admin != null)
+        {
+            claims.Add(
+                new Claim(
+                    "AdminType",
+                    admin.Type.ToString()));
+        }
+
+
+        var identity = new ClaimsIdentity(
+            claims,
+            "Cookies");
+
+        var principal =
+            new ClaimsPrincipal(identity);
+
+
+        // =====================================================
+        // SIGN IN
+        // =====================================================
+
+        await HttpContext.SignInAsync(
+            "Cookies",
+            principal);
+
+
+        // =====================================================
+        // KEEP SESSION FOR EXISTING CODE
+        // =====================================================
+
         HttpContext.Session.SetInt32(
             "UserId",
@@ -133,25 +212,15 @@
             user.Username);
 
-        // Determine account type from the actual
-        // ADMINS / CONSUMERS tables.
-        var admin = _context.Admins
-            .FirstOrDefault(x => x.UserId == user.UserId);
+        HttpContext.Session.SetString(
+            "Role",
+            role);
 
         if (admin != null)
         {
-            HttpContext.Session.SetString(
-                "Role",
-                "Admin");
-
             HttpContext.Session.SetString(
                 "AdminType",
                 admin.Type.ToString());
         }
-        else
-        {
-            HttpContext.Session.SetString(
-                "Role",
-                "Consumer");
-        }
+
 
         return RedirectToAction(
@@ -161,10 +230,195 @@
 
 
-    // ==============================
+    // =========================================================
+    // PROFILE - GET
+    // =========================================================
+
+    [HttpGet]
+    public IActionResult Profile()
+    {
+        // Get logged-in user's ID from authentication claims.
+        var userIdClaim = User.FindFirst(
+            ClaimTypes.NameIdentifier);
+
+        if (userIdClaim == null)
+            return RedirectToAction(nameof(Login));
+
+        if (!long.TryParse(
+                userIdClaim.Value,
+                out var userId))
+        {
+            return RedirectToAction(nameof(Login));
+        }
+
+
+        // Load user + consumer information.
+        var user = _context.Users
+            .Include(x => x.Consumer)
+            .FirstOrDefault(x =>
+                x.UserId == userId);
+
+        if (user == null)
+            return NotFound();
+
+
+        var model = new ProfileViewModel
+        {
+            UserId = user.UserId,
+
+            Username = user.Username,
+
+            Email = user.Email,
+
+            TelephoneNumber =
+                user.TelephoneNumber,
+
+            ShippingAddress =
+                user.ShippingAddress,
+
+            DateCreated =
+                user.DateCreated,
+
+            PointsCollected =
+                user.Consumer?.PointsCollected ?? 0
+        };
+
+
+        return View(model);
+    }
+
+
+    // =========================================================
+    // PROFILE - POST
+    // =========================================================
+
+    [HttpPost]
+    [ValidateAntiForgeryToken]
+    public async Task<IActionResult> Profile(
+        ProfileViewModel model)
+    {
+        // Get the currently authenticated user.
+        var userIdClaim = User.FindFirst(
+            ClaimTypes.NameIdentifier);
+
+        if (userIdClaim == null)
+            return RedirectToAction(nameof(Login));
+
+        if (!long.TryParse(
+                userIdClaim.Value,
+                out var userId))
+        {
+            return RedirectToAction(nameof(Login));
+        }
+
+
+        if (!ModelState.IsValid)
+            return View(model);
+
+
+        var user = _context.Users
+            .FirstOrDefault(x =>
+                x.UserId == userId);
+
+        if (user == null)
+            return NotFound();
+
+
+        // =====================================================
+        // CHECK EMAIL
+        // =====================================================
+
+        var emailExists = _context.Users.Any(x =>
+            x.Email == model.Email &&
+            x.UserId != userId);
+
+        if (emailExists)
+        {
+            ModelState.AddModelError(
+                nameof(model.Email),
+                "This email is already registered.");
+
+            return View(model);
+        }
+
+
+        // =====================================================
+        // UPDATE USER
+        // =====================================================
+
+        user.Email = model.Email;
+
+        user.TelephoneNumber =
+            model.TelephoneNumber;
+
+        user.ShippingAddress =
+            model.ShippingAddress;
+
+
+        _context.SaveChanges();
+
+
+        TempData["Success"] =
+            "Your profile has been updated successfully.";
+
+
+        // =====================================================
+        // REFRESH EMAIL CLAIM
+        // =====================================================
+
+        var claims = new List<Claim>
+        {
+            new Claim(
+                ClaimTypes.NameIdentifier,
+                user.UserId.ToString()),
+
+            new Claim(
+                ClaimTypes.Name,
+                user.Username),
+
+            new Claim(
+                ClaimTypes.Email,
+                user.Email),
+
+            new Claim(
+                ClaimTypes.Role,
+                User.IsInRole("Admin")
+                    ? "Admin"
+                    : "Consumer")
+        };
+
+
+        var identity = new ClaimsIdentity(
+            claims,
+            "Cookies");
+
+        var principal =
+            new ClaimsPrincipal(identity);
+
+
+        await HttpContext.SignInAsync(
+            "Cookies",
+            principal);
+
+
+        // Keep session synchronized.
+        HttpContext.Session.SetString(
+            "Username",
+            user.Username);
+
+
+        return RedirectToAction(nameof(Profile));
+    }
+
+
+    // =========================================================
     // LOGOUT
-    // ==============================
-
-    public IActionResult Logout()
-    {
+    // =========================================================
+
+    [HttpPost]
+    [ValidateAntiForgeryToken]
+    public async Task<IActionResult> Logout()
+    {
+        await HttpContext.SignOutAsync("Cookies");
+
         HttpContext.Session.Clear();
 
Index: KernelRecordsMVC.Web/Controllers/AdminController.cs
===================================================================
--- KernelRecordsMVC.Web/Controllers/AdminController.cs	(revision 08aefc6f3b51f4bfcb7471a2fd319c064171b1c5)
+++ KernelRecordsMVC.Web/Controllers/AdminController.cs	(revision fa0fbaf2cdd288351eeaf3d4d17d8a6158ea5adc)
@@ -1,11 +1,10 @@
-﻿using Microsoft.AspNetCore.Mvc;
+﻿using KernelRecordsMVC.Application.ViewModels;
+using KernelRecordsMVC.Domain.Enums;
+using KernelRecordsMVC.Infrastructure.Data;
+using KernelRecordsMVC.Models;
+using Microsoft.AspNetCore.Mvc;
 using Microsoft.EntityFrameworkCore;
-using KernelRecordsMVC.Data;
-using KernelRecordsMVC.Models;
-using KernelRecordsMVC.Domain.Enums;
-using KernelRecordsMVC.Application.ViewModels;
-using Microsoft.AspNetCore.Http;
-
-namespace KernelRecordsMVC.Controllers;
+
+namespace KernelRecordsMVC.Web.Controllers;
 
 public class AdminController : Controller
@@ -19,7 +18,7 @@
 
 
-    // ==========================================
+    // =========================================================
     // ADMIN DASHBOARD
-    // ==========================================
+    // =========================================================
 
     [HttpGet]
@@ -29,12 +28,50 @@
             return Forbid();
 
+
+        ViewBag.ProductCount =
+            _context.Products.Count();
+
+        ViewBag.ReleaseCount =
+            _context.Releases.Count();
+
+        ViewBag.OutOfStockCount =
+            _context.Products.Count(x =>
+                x.Stock <= 0);
+
+        ViewBag.LowStockCount =
+            _context.Products.Count(x =>
+                x.Stock > 0 &&
+                x.Stock <= 5);
+
+
         return View();
     }
 
 
-    // ==========================================
-    // UC008
-    // NEW PRODUCT - GET
-    // ==========================================
+    // =========================================================
+    // PRODUCTS
+    // =========================================================
+
+    [HttpGet]
+    public IActionResult Products()
+    {
+        if (!IsProductManager())
+            return Forbid();
+
+
+        var products = _context.Products
+            .Include(p => p.Release)
+            .OrderBy(p => p.Release.Title)
+            .ThenBy(p => p.Format)
+            .ToList();
+
+
+        return View(products);
+    }
+
+
+    // =========================================================
+    // CREATE PRODUCT - GET
+    // =========================================================
 
     [HttpGet]
@@ -44,16 +81,16 @@
             return Forbid();
 
-        ViewBag.Releases = _context.Releases
-            .OrderBy(x => x.Title)
-            .ToList();
-
-        return View(new CreateProductViewModel());
-    }
-
-
-    // ==========================================
-    // UC008
-    // NEW PRODUCT - POST
-    // ==========================================
+
+        LoadReleases();
+
+
+        return View(
+            new CreateProductViewModel());
+    }
+
+
+    // =========================================================
+    // CREATE PRODUCT - POST
+    // =========================================================
 
     [HttpPost]
@@ -65,89 +102,852 @@
             return Forbid();
 
+
         if (!ModelState.IsValid)
         {
             LoadReleases();
+
             return View(model);
         }
 
 
-        // Make sure the release actually exists.
         var release = _context.Releases
             .FirstOrDefault(x =>
                 x.ReleaseId == model.ReleaseId);
 
+
         if (release == null)
         {
             ModelState.AddModelError(
-                "ReleaseId",
+                nameof(model.ReleaseId),
                 "The selected release does not exist.");
 
+
             LoadReleases();
+
+
             return View(model);
         }
 
 
-        // Don't allow duplicate format for
-        // the same release.
-        var alreadyExists = _context.Products.Any(p =>
-            p.ReleaseId == model.ReleaseId &&
-            p.Format == model.Format);
+        var alreadyExists =
+            _context.Products.Any(p =>
+                p.ReleaseId == model.ReleaseId &&
+                p.Format == model.Format);
+
 
         if (alreadyExists)
         {
             ModelState.AddModelError(
-                "Format",
+                nameof(model.Format),
                 "This release already has a product in this format.");
 
+
             LoadReleases();
+
+
             return View(model);
         }
 
 
-        var product = new Product
-        {
-            ProductId = GetNextProductId(),
-
-            ReleaseId = model.ReleaseId,
-
-            Format = model.Format,
-
-            Price = model.Price,
-
-            ProductDescription =
-                model.ProductDescription,
-
-            Stock = model.Stock
-        };
-
-
-        _context.Products.Add(product);
-
-        _context.SaveChanges();
-
-
-        // Record the modification.
-        CreateModification(
-            ModificationType.CREATE,
-            product.ProductId);
-
-
-        TempData["Success"] =
-            "Product created successfully.";
-
-
-        return RedirectToAction(
-            nameof(Index));
-    }
-
-
-    // ==========================================
-    // HELPERS
-    // ==========================================
+        using var transaction =
+            _context.Database.BeginTransaction();
+
+
+        try
+        {
+            var product = new Product
+            {
+                ProductId =
+                    GetNextProductId(),
+
+                ReleaseId =
+                    model.ReleaseId,
+
+                Format =
+                    model.Format,
+
+                Price =
+                    model.Price,
+
+                ProductDescription =
+                    model.ProductDescription,
+
+                Stock =
+                    model.Stock
+            };
+
+
+            _context.Products.Add(product);
+
+            _context.SaveChanges();
+
+
+            CreateModification(
+                ModificationType.CREATE,
+                product.ProductId);
+
+
+            transaction.Commit();
+
+
+            TempData["Success"] =
+                $"{release.Title} ({product.Format}) was created successfully.";
+
+
+            return RedirectToAction(
+                nameof(Products));
+        }
+        catch
+        {
+            transaction.Rollback();
+
+            throw;
+        }
+    }
+
+
+    // =========================================================
+    // EDIT PRODUCT - GET
+    // =========================================================
+
+    [HttpGet]
+    public IActionResult EditProduct(long id)
+    {
+        if (!IsProductManager())
+            return Forbid();
+
+
+        var product = _context.Products
+            .Include(p => p.Release)
+            .FirstOrDefault(p =>
+                p.ProductId == id);
+
+
+        if (product == null)
+            return NotFound();
+
+
+        var model =
+            new EditProductViewModel
+            {
+                ProductId =
+                    product.ProductId,
+
+                ReleaseId =
+                    product.ReleaseId,
+
+                ReleaseTitle =
+                    product.Release.Title,
+
+                Format =
+                    product.Format,
+
+                Price =
+                    product.Price,
+
+                ProductDescription =
+                    product.ProductDescription,
+
+                Stock =
+                    product.Stock,
+
+                ModificationType =
+                    ModificationType.UPDATE
+            };
+
+
+        return View(model);
+    }
+
+
+    // =========================================================
+    // EDIT PRODUCT - POST
+    // =========================================================
+
+    [HttpPost]
+    [ValidateAntiForgeryToken]
+    public IActionResult EditProduct(
+        EditProductViewModel model)
+    {
+        if (!IsProductManager())
+            return Forbid();
+
+
+        var product = _context.Products
+            .Include(p => p.Release)
+            .FirstOrDefault(p =>
+                p.ProductId == model.ProductId);
+
+
+        if (product == null)
+            return NotFound();
+
+
+        // These values come from the database.
+        model.ReleaseId =
+            product.ReleaseId;
+
+        model.ReleaseTitle =
+            product.Release.Title;
+
+        model.Format =
+            product.Format;
+
+
+        // Display-only fields should not block
+        // validation of the submitted form.
+        ModelState.Remove(
+            nameof(model.ReleaseId));
+
+        ModelState.Remove(
+            nameof(model.ReleaseTitle));
+
+        ModelState.Remove(
+            nameof(model.Format));
+
+
+        // =====================================================
+        // DISCOUNT VALIDATION
+        // =====================================================
+
+        if (model.ModificationType ==
+            ModificationType.DISCOUNT)
+        {
+            if (!model.Discount.HasValue)
+            {
+                ModelState.AddModelError(
+                    nameof(model.Discount),
+                    "Please enter a discount percentage.");
+            }
+            else if (
+                model.Discount.Value <= 0 ||
+                model.Discount.Value >= 100)
+            {
+                ModelState.AddModelError(
+                    nameof(model.Discount),
+                    "Discount must be greater than 0 and less than 100.");
+            }
+        }
+
+
+        if (!ModelState.IsValid)
+            return View(model);
+
+
+        using var transaction =
+            _context.Database.BeginTransaction();
+
+
+        try
+        {
+            // =================================================
+            // APPLY DISCOUNT
+            // =================================================
+
+            if (model.ModificationType ==
+                ModificationType.DISCOUNT)
+            {
+                var discountPercentage =
+                    model.Discount!.Value;
+
+
+                var oldPrice =
+                    product.Price;
+
+
+                var discountAmount =
+                    oldPrice *
+                    (discountPercentage / 100m);
+
+
+                var newPrice =
+                    oldPrice - discountAmount;
+
+
+                newPrice =
+                    Math.Round(
+                        newPrice,
+                        2,
+                        MidpointRounding.AwayFromZero);
+
+
+                // Product.Price becomes the
+                // actual current sale price.
+                product.Price =
+                    newPrice;
+
+
+                _context.Products.Update(
+                    product);
+
+                _context.SaveChanges();
+
+
+                CreateModification(
+                    ModificationType.DISCOUNT,
+                    product.ProductId,
+                    discountPercentage);
+
+
+                transaction.Commit();
+
+
+                TempData["Success"] =
+                    $"{discountPercentage:0.##}% discount applied. " +
+                    $"Price changed from {oldPrice:C} to {newPrice:C}.";
+
+
+                return RedirectToAction(
+                    nameof(Products));
+            }
+
+
+            // =================================================
+            // NORMAL UPDATE
+            // =================================================
+
+            product.Price =
+                model.Price;
+
+            product.ProductDescription =
+                model.ProductDescription;
+
+            product.Stock =
+                model.Stock;
+
+
+            _context.Products.Update(
+                product);
+
+            _context.SaveChanges();
+
+
+            CreateModification(
+                ModificationType.UPDATE,
+                product.ProductId);
+
+
+            transaction.Commit();
+
+
+            TempData["Success"] =
+                $"{product.Release.Title} was updated successfully.";
+
+
+            return RedirectToAction(
+                nameof(Products));
+        }
+        catch
+        {
+            transaction.Rollback();
+
+            throw;
+        }
+    }
+
+
+    // =========================================================
+    // CREATE RELEASE - GET
+    // =========================================================
+
+    [HttpGet]
+    public IActionResult CreateRelease()
+    {
+        if (!IsAdmin())
+            return Forbid();
+
+
+        LoadArtists();
+
+
+        var model =
+            new CreateReleaseViewModel
+            {
+                ReleaseDate =
+                    DateTime.Today,
+
+                ReleaseType =
+                    "ALBUM",
+
+                // Start an album with one empty track.
+                Tracks =
+                    new List<CreateTrackViewModel>
+                    {
+                        new CreateTrackViewModel()
+                    }
+            };
+
+
+        return View(model);
+    }
+
+
+    // =========================================================
+    // CREATE RELEASE - POST
+    // =========================================================
+
+    [HttpPost]
+    [ValidateAntiForgeryToken]
+    public IActionResult CreateRelease(
+        CreateReleaseViewModel model)
+    {
+        if (!IsAdmin())
+            return Forbid();
+
+
+        // =====================================================
+        // RELEASE TYPE VALIDATION
+        // =====================================================
+
+        if (model.ReleaseType != "ALBUM" &&
+            model.ReleaseType != "SINGLE")
+        {
+            ModelState.AddModelError(
+                nameof(model.ReleaseType),
+                "Please select Album or Single.");
+        }
+
+
+        // =====================================================
+        // MAIN ARTIST VALIDATION
+        // =====================================================
+
+        var mainArtistExists =
+            _context.Artists.Any(a =>
+                a.ArtistId ==
+                model.MainArtistId);
+
+
+        if (!mainArtistExists)
+        {
+            ModelState.AddModelError(
+                nameof(model.MainArtistId),
+                "Please select a valid main artist.");
+        }
+
+
+        // =====================================================
+        // FEATURED ARTISTS
+        // =====================================================
+
+        model.FeaturedArtistIds ??=
+            new List<long>();
+
+
+        model.FeaturedArtistIds =
+            model.FeaturedArtistIds
+                .Distinct()
+                .ToList();
+
+
+        // Do not allow the main artist
+        // to also be a featured artist.
+        model.FeaturedArtistIds.Remove(
+            model.MainArtistId);
+
+
+        // Validate that featured artists exist.
+        if (model.FeaturedArtistIds.Count > 0)
+        {
+            var validFeaturedArtistCount =
+                _context.Artists.Count(a =>
+                    model.FeaturedArtistIds
+                        .Contains(a.ArtistId));
+
+
+            if (validFeaturedArtistCount !=
+                model.FeaturedArtistIds.Count)
+            {
+                ModelState.AddModelError(
+                    nameof(model.FeaturedArtistIds),
+                    "One or more featured artists are invalid.");
+            }
+        }
+
+
+        // =====================================================
+        // SINGLE VALIDATION
+        // =====================================================
+
+        if (model.ReleaseType == "SINGLE")
+        {
+            if (string.IsNullOrWhiteSpace(
+                model.SingleDuration))
+            {
+                ModelState.AddModelError(
+                    nameof(model.SingleDuration),
+                    "Duration is required for a single.");
+            }
+        }
+
+
+        // =====================================================
+        // ALBUM VALIDATION
+        // =====================================================
+
+        if (model.ReleaseType == "ALBUM")
+        {
+            model.Tracks ??=
+                new List<CreateTrackViewModel>();
+
+
+            // Remove completely empty rows.
+            model.Tracks =
+                model.Tracks
+                    .Where(t =>
+                        !string.IsNullOrWhiteSpace(
+                            t.SongName) ||
+                        !string.IsNullOrWhiteSpace(
+                            t.SongDuration))
+                    .ToList();
+
+
+            if (model.Tracks.Count == 0)
+            {
+                ModelState.AddModelError(
+                    nameof(model.Tracks),
+                    "An album must contain at least one track.");
+            }
+
+
+            for (var i = 0;
+                 i < model.Tracks.Count;
+                 i++)
+            {
+                var track =
+                    model.Tracks[i];
+
+
+                if (string.IsNullOrWhiteSpace(
+                    track.SongName))
+                {
+                    ModelState.AddModelError(
+                        $"Tracks[{i}].SongName",
+                        "Track name is required.");
+                }
+
+
+                if (string.IsNullOrWhiteSpace(
+                    track.SongDuration))
+                {
+                    ModelState.AddModelError(
+                        $"Tracks[{i}].SongDuration",
+                        "Track duration is required.");
+                }
+
+
+                track.ArtistIds ??=
+                    new List<long>();
+
+
+                track.ArtistIds =
+                    track.ArtistIds
+                        .Distinct()
+                        .ToList();
+
+
+                // Validate selected track artists.
+                if (track.ArtistIds.Count > 0)
+                {
+                    var validTrackArtistCount =
+                        _context.Artists.Count(a =>
+                            track.ArtistIds.Contains(
+                                a.ArtistId));
+
+
+                    if (validTrackArtistCount !=
+                        track.ArtistIds.Count)
+                    {
+                        ModelState.AddModelError(
+                            $"Tracks[{i}].ArtistIds",
+                            "One or more selected track artists are invalid.");
+                    }
+                }
+            }
+        }
+
+
+        if (!ModelState.IsValid)
+        {
+            LoadArtists();
+
+            return View(model);
+        }
+
+
+        using var transaction =
+            _context.Database.BeginTransaction();
+
+
+        try
+        {
+            // =================================================
+            // CREATE RELEASE
+            // =================================================
+
+            var release =
+                new Release
+                {
+                    ReleaseId =
+                        GetNextReleaseId(),
+
+                    Title =
+                        model.Title.Trim(),
+
+                    RecordLabel =
+                        string.IsNullOrWhiteSpace(
+                            model.RecordLabel)
+                            ? null
+                            : model.RecordLabel.Trim(),
+
+                    Genre =
+                        model.Genre.Trim(),
+
+                    ReleaseDate =
+                        model.ReleaseDate,
+
+                    CoverPhoto =
+                        model.CoverPhoto.Trim()
+                };
+
+
+            _context.Releases.Add(
+                release);
+
+            _context.SaveChanges();
+
+
+            // =================================================
+            // MAIN RELEASE ARTIST
+            // =================================================
+
+            var mainReleaseArtist =
+                new ReleaseArtist
+                {
+                    ReleaseId =
+                        release.ReleaseId,
+
+                    ArtistId =
+                        model.MainArtistId,
+
+                    ReleaseOrdinal = 1,
+
+                    Type =
+                        ArtistReleaseType.MAIN
+                };
+
+
+            _context.ReleaseArtists.Add(
+                mainReleaseArtist);
+
+
+            // =================================================
+            // FEATURED RELEASE ARTISTS
+            // =================================================
+
+            long releaseOrdinal = 2;
+
+
+            foreach (var artistId
+                     in model.FeaturedArtistIds)
+            {
+                var featuredArtist =
+                    new ReleaseArtist
+                    {
+                        ReleaseId =
+                            release.ReleaseId,
+
+                        ArtistId =
+                            artistId,
+
+                        ReleaseOrdinal =
+                            releaseOrdinal++,
+
+                        Type =
+                            ArtistReleaseType.FEATURE
+                    };
+
+
+                _context.ReleaseArtists.Add(
+                    featuredArtist);
+            }
+
+
+            _context.SaveChanges();
+
+
+            // =================================================
+            // ALBUM
+            // =================================================
+
+            if (model.ReleaseType == "ALBUM")
+            {
+                var album =
+                    new Album
+                    {
+                        ReleaseId =
+                            release.ReleaseId
+                    };
+
+
+                _context.Albums.Add(
+                    album);
+
+                _context.SaveChanges();
+
+
+                // =============================================
+                // TRACKS
+                // =============================================
+
+                foreach (var trackModel
+                         in model.Tracks)
+                {
+                    var song =
+                        new Song
+                        {
+                            SongId =
+                                GetNextSongId(),
+
+                            SongName =
+                                trackModel
+                                    .SongName
+                                    .Trim(),
+
+                            SongDuration =
+                                trackModel
+                                    .SongDuration
+                                    .Trim()
+                        };
+
+
+                    _context.Songs.Add(
+                        song);
+
+                    _context.SaveChanges();
+
+
+                    // =========================================
+                    // LINK SONG TO ALBUM
+                    // =========================================
+
+                    var albumSong =
+                        new AlbumSong
+                        {
+                            AlbumId =
+                                release.ReleaseId,
+
+                            SongId =
+                                song.SongId
+                        };
+
+
+                    _context.AlbumSongs.Add(
+                        albumSong);
+
+
+                    // =========================================
+                    // SONG ARTISTS
+                    // =========================================
+
+                    var trackArtistIds =
+                        trackModel.ArtistIds
+                            .Distinct()
+                            .ToList();
+
+
+                    // If no track artists were selected,
+                    // automatically use the release's
+                    // main artist.
+                    if (trackArtistIds.Count == 0)
+                    {
+                        trackArtistIds.Add(
+                            model.MainArtistId);
+                    }
+
+
+                    long songOrdinal = 1;
+
+
+                    foreach (var artistId
+                             in trackArtistIds)
+                    {
+                        var songArtist =
+                            new SongArtist
+                            {
+                                SongId =
+                                    song.SongId,
+
+                                ArtistId =
+                                    artistId,
+
+                                SongOrdinal =
+                                    songOrdinal++
+                            };
+
+
+                        _context.SongArtists.Add(
+                            songArtist);
+                    }
+
+
+                    _context.SaveChanges();
+                }
+            }
+
+
+            // =================================================
+            // SINGLE
+            // =================================================
+
+            else
+            {
+                var single =
+                    new SingleRelease
+                    {
+                        ReleaseId =
+                            release.ReleaseId,
+
+                        Duration =
+                            model.SingleDuration!
+                                .Trim()
+                    };
+
+
+                _context.SingleReleases.Add(
+                    single);
+
+                _context.SaveChanges();
+            }
+
+
+            transaction.Commit();
+
+
+            TempData["Success"] =
+                $"{release.Title} was created successfully.";
+
+
+            return RedirectToAction(
+                nameof(Index));
+        }
+        catch
+        {
+            transaction.Rollback();
+
+            throw;
+        }
+    }
+
+
+    // =========================================================
+    // AUTHORIZATION HELPERS
+    // =========================================================
 
     private bool IsAdmin()
     {
         return HttpContext.Session
-            .GetString("Role") == "Admin";
+            .GetString("Role") ==
+            "Admin";
     }
 
@@ -155,33 +955,80 @@
     private bool IsProductManager()
     {
-        var role = HttpContext.Session
-            .GetString("Role");
-
-        var adminType = HttpContext.Session
-            .GetString("AdminType");
+        var role =
+            HttpContext.Session
+                .GetString("Role");
+
+        var adminType =
+            HttpContext.Session
+                .GetString("AdminType");
+
 
         return role == "Admin" &&
-               (adminType == "PRODUCT_MANAGER" ||
-                adminType == "SUPER_ADMIN");
-    }
-
+               (
+                   adminType == "PRODUCT_MANAGER" ||
+                   adminType == "SUPER_ADMIN"
+               );
+    }
+
+
+    // =========================================================
+    // CURRENT ADMIN ID
+    // =========================================================
 
     private long? GetCurrentUserId()
     {
-        var value = HttpContext.Session
-            .GetString("UserId");
-
-        if (long.TryParse(value, out var userId))
-            return userId;
-
-        return null;
-    }
-
+        var userId =
+            HttpContext.Session
+                .GetInt32("UserId");
+
+
+        if (!userId.HasValue)
+            return null;
+
+
+        return userId.Value;
+    }
+
+
+    // =========================================================
+    // LOAD RELEASES
+    // =========================================================
+
+    private void LoadReleases()
+    {
+        ViewBag.Releases =
+            _context.Releases
+                .OrderBy(x =>
+                    x.Title)
+                .ToList();
+    }
+
+
+    // =========================================================
+    // LOAD ARTISTS
+    // =========================================================
+
+    private void LoadArtists()
+    {
+        ViewBag.Artists =
+            _context.Artists
+                .OrderBy(a =>
+                    a.ArtistName)
+                .ToList();
+    }
+
+
+    // =========================================================
+    // NEXT PRODUCT ID
+    // =========================================================
 
     private long GetNextProductId()
     {
-        var maxId = _context.Products
-            .Select(x => (long?)x.ProductId)
-            .Max();
+        var maxId =
+            _context.Products
+                .Select(x =>
+                    (long?)x.ProductId)
+                .Max();
+
 
         return (maxId ?? 0) + 1;
@@ -189,11 +1036,58 @@
 
 
-    private void LoadReleases()
-    {
-        ViewBag.Releases = _context.Releases
-            .OrderBy(x => x.Title)
-            .ToList();
-    }
-
+    // =========================================================
+    // NEXT RELEASE ID
+    // =========================================================
+
+    private long GetNextReleaseId()
+    {
+        var maxId =
+            _context.Releases
+                .Select(x =>
+                    (long?)x.ReleaseId)
+                .Max();
+
+
+        return (maxId ?? 0) + 1;
+    }
+
+
+    // =========================================================
+    // NEXT SONG ID
+    // =========================================================
+
+    private long GetNextSongId()
+    {
+        var maxId =
+            _context.Songs
+                .Select(x =>
+                    (long?)x.SongId)
+                .Max();
+
+
+        return (maxId ?? 0) + 1;
+    }
+
+
+    // =========================================================
+    // NEXT MODIFICATION ID
+    // =========================================================
+
+    private long GetNextModificationId()
+    {
+        var maxId =
+            _context.Modifications
+                .Select(x =>
+                    (long?)x.ModificationId)
+                .Max();
+
+
+        return (maxId ?? 0) + 1;
+    }
+
+
+    // =========================================================
+    // CREATE MODIFICATION RECORD
+    // =========================================================
 
     private void CreateModification(
@@ -202,26 +1096,37 @@
         decimal? discount = null)
     {
-        var adminId = GetCurrentUserId();
+        var adminId =
+            GetCurrentUserId();
+
 
         if (adminId == null)
-            return;
-
-
-        var modification = new Modification
-        {
-            ModificationId =
-                GetNextModificationId(),
-
-            AdminId = adminId.Value,
-
-            DateModified = DateTime.Today,
-
-            TypeOfModification = type,
-
-            Discount = discount
-        };
-
-
-        _context.Modifications.Add(modification);
+        {
+            throw new InvalidOperationException(
+                "The current admin could not be identified.");
+        }
+
+
+        var modification =
+            new Modification
+            {
+                ModificationId =
+                    GetNextModificationId(),
+
+                AdminId =
+                    adminId.Value,
+
+                DateModified =
+                    DateTime.Today,
+
+                TypeOfModification =
+                    type,
+
+                Discount =
+                    discount
+            };
+
+
+        _context.Modifications.Add(
+            modification);
 
         _context.SaveChanges();
@@ -234,5 +1139,6 @@
                     modification.ModificationId,
 
-                ProductId = productId
+                ProductId =
+                    productId
             };
 
@@ -243,146 +1149,3 @@
         _context.SaveChanges();
     }
-
-
-    private long GetNextModificationId()
-    {
-        var maxId = _context.Modifications
-            .Select(x => (long?)x.ModificationId)
-            .Max();
-
-        return (maxId ?? 0) + 1;
-    }
-    // ==========================================
-// UC009
-// MODIFY PRODUCT - GET
-// ==========================================
-
-    [HttpGet]
-    public IActionResult EditProduct(long id)
-    {
-        if (!IsProductManager())
-            return Forbid();
-
-        var product = _context.Products
-            .Include(p => p.Release)
-            .FirstOrDefault(p => p.ProductId == id);
-
-        if (product == null)
-            return NotFound();
-
-        var model = new EditProductViewModel
-        {
-            ProductId = product.ProductId,
-            ReleaseId = product.ReleaseId,
-            ReleaseTitle = product.Release.Title,
-            Format = product.Format,
-            Price = product.Price,
-            ProductDescription = product.ProductDescription,
-            Stock = product.Stock
-        };
-
-        return View(model);
-    }
-    // ==========================================
-// UC009
-// MODIFY PRODUCT - POST
-// ==========================================
-
-[HttpPost]
-[ValidateAntiForgeryToken]
-public IActionResult EditProduct(
-    EditProductViewModel model)
-{
-    if (!IsProductManager())
-        return Forbid();
-
-    var product = _context.Products
-        .Include(p => p.Release)
-        .FirstOrDefault(p =>
-            p.ProductId == model.ProductId);
-
-    if (product == null)
-        return NotFound();
-
-    if (!ModelState.IsValid)
-    {
-        model.ReleaseTitle = product.Release.Title;
-        model.ReleaseId = product.ReleaseId;
-        model.Format = product.Format;
-
-        return View(model);
-    }
-
-
-    // ==========================================
-    // DISCOUNT
-    // ==========================================
-
-    if (model.ModificationType ==
-        ModificationType.DISCOUNT)
-    {
-        if (!model.Discount.HasValue)
-        {
-            ModelState.AddModelError(
-                "Discount",
-                "Please enter a discount percentage.");
-
-            model.ReleaseTitle = product.Release.Title;
-            model.ReleaseId = product.ReleaseId;
-            model.Format = product.Format;
-
-            return View(model);
-        }
-
-        product.Price =
-            product.Price *
-            (1 - model.Discount.Value / 100m);
-    }
-    else
-    {
-        // ==========================================
-        // NORMAL UPDATE
-        // ==========================================
-
-        product.Price = model.Price;
-
-        product.ProductDescription =
-            model.ProductDescription;
-
-        product.Stock = model.Stock;
-    }
-
-
-    _context.SaveChanges();
-
-
-    // ==========================================
-    // RECORD MODIFICATION
-    // ==========================================
-
-    CreateModification(
-        model.ModificationType,
-        product.ProductId,
-        model.Discount);
-
-
-    TempData["Success"] =
-        "Product modified successfully.";
-
-    return RedirectToAction(nameof(Index));
 }
-[HttpGet]
-public IActionResult Products()
-{
-    if (!IsProductManager())
-        return Forbid();
-
-    var products = _context.Products
-        .Include(p => p.Release)
-        .OrderBy(p => p.Release.Title)
-        .ThenBy(p => p.Format)
-        .ToList();
-
-    return View(products);
-}
-}
Index: KernelRecordsMVC.Web/Controllers/HomeController.cs
===================================================================
--- KernelRecordsMVC.Web/Controllers/HomeController.cs	(revision 08aefc6f3b51f4bfcb7471a2fd319c064171b1c5)
+++ KernelRecordsMVC.Web/Controllers/HomeController.cs	(revision fa0fbaf2cdd288351eeaf3d4d17d8a6158ea5adc)
@@ -1,8 +1,7 @@
 using System.Diagnostics;
+using KernelRecordsMVC.Models;
 using Microsoft.AspNetCore.Mvc;
-using KernelRecordsMVC.Models;
-using Microsoft.Extensions.Logging;
 
-namespace KernelRecordsMVC.Controllers;
+namespace KernelRecordsMVC.Web.Controllers;
 
 public class HomeController : Controller
Index: KernelRecordsMVC.Web/Controllers/OrderController.cs
===================================================================
--- KernelRecordsMVC.Web/Controllers/OrderController.cs	(revision 08aefc6f3b51f4bfcb7471a2fd319c064171b1c5)
+++ KernelRecordsMVC.Web/Controllers/OrderController.cs	(revision fa0fbaf2cdd288351eeaf3d4d17d8a6158ea5adc)
@@ -1,12 +1,10 @@
-﻿using Microsoft.AspNetCore.Http;
-
-namespace KernelRecordsMVC.Controllers;
-
+﻿using KernelRecordsMVC.Application.ViewModels;
+using KernelRecordsMVC.Domain.Enums;
+using KernelRecordsMVC.Infrastructure.Data;
+using KernelRecordsMVC.Models;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.EntityFrameworkCore;
-using KernelRecordsMVC.Data;
-using KernelRecordsMVC.Models;
-using KernelRecordsMVC.Domain.Enums;
-using KernelRecordsMVC.Application.ViewModels;
+
+namespace KernelRecordsMVC.Web.Controllers;
 
 public class OrderController : Controller
@@ -67,17 +65,9 @@
             order = new Order
             {
-                OrderId = GetNextOrderId(),
                 UserId = userId.Value,
-
-                // The actual payment method will be
-                // selected during checkout.
                 PaymentMethod = PaymentMethodType.CARD,
-
                 PurchaseDate = DateTime.Today,
-
                 PointsEarned = 0,
-
                 PointsUsed = null,
-
                 Status = OrderStatusType.PENDING
             };
@@ -113,4 +103,29 @@
         else
         {
+            var discount = _context.ModificationProducts
+                .Include(mp => mp.Modification)
+                .Where(mp =>
+                    mp.ProductId == product.ProductId &&
+                    mp.Modification.TypeOfModification ==
+                    ModificationType.DISCOUNT &&
+                    mp.Modification.Discount.HasValue &&
+                    mp.Modification.Discount.Value > 0)
+                .Select(mp => mp.Modification)
+                .OrderByDescending(m => m.DateModified)
+                .FirstOrDefault();
+
+            var priceAtPurchase = product.Price;
+
+            if (discount != null)
+            {
+                priceAtPurchase =
+                    product.Price -
+                    (product.Price * discount.Discount!.Value / 100m);
+
+                priceAtPurchase = Math.Round(
+                    priceAtPurchase,
+                    2);
+            }
+
             var orderProduct = new OrderProduct
             {
@@ -186,5 +201,7 @@
 
                     Stock =
-                        op.Product.Stock
+                        op.Product.Stock,
+                    
+                    CoverPhoto = op.Product.Release.CoverPhoto
                 })
                 .ToList();
@@ -294,25 +311,10 @@
     private long? GetUserId()
     {
-        var value =
-            HttpContext.Session.GetString("UserId");
-
-        if (long.TryParse(value, out var userId))
-            return userId;
+        var userId = HttpContext.Session.GetInt32("UserId");
+
+        if (userId.HasValue)
+            return userId.Value;
 
         return null;
     }
-
-
-    // ==========================================
-    // ORDER ID
-    // ==========================================
-
-    private long GetNextOrderId()
-    {
-        var maxId = _context.Orders
-            .Select(x => (long?)x.OrderId)
-            .Max();
-
-        return (maxId ?? 0) + 1;
-    }
 }
Index: KernelRecordsMVC.Web/Controllers/ReleaseController.cs
===================================================================
--- KernelRecordsMVC.Web/Controllers/ReleaseController.cs	(revision 08aefc6f3b51f4bfcb7471a2fd319c064171b1c5)
+++ KernelRecordsMVC.Web/Controllers/ReleaseController.cs	(revision fa0fbaf2cdd288351eeaf3d4d17d8a6158ea5adc)
@@ -1,7 +1,8 @@
-﻿using Microsoft.AspNetCore.Mvc;
+﻿using KernelRecordsMVC.Domain.Enums;
+using KernelRecordsMVC.Infrastructure.Data;
+using Microsoft.AspNetCore.Mvc;
 using Microsoft.EntityFrameworkCore;
-using KernelRecordsMVC.Data;
 
-namespace KernelRecordsMVC.Controllers;
+namespace KernelRecordsMVC.Web.Controllers;
 
 public class ReleaseController : Controller
@@ -14,19 +15,37 @@
     }
 
-    // UC003 - Browse Releases
-    public IActionResult Index(string? search, string? genre)
+    // =========================================================
+    // BROWSE RELEASES
+    // =========================================================
+
+    public IActionResult Index(
+        string? search,
+        string? genre,
+        string? format,
+        string? type,
+        string? sort)
     {
         var query = _context.Releases
             .Include(r => r.Products)
             .Include(r => r.ReleaseArtists)
-            .ThenInclude(ra => ra.Artist)
+                .ThenInclude(ra => ra.Artist)
+            .Include(r => r.Album)
+            .Include(r => r.SingleRelease)
             .AsQueryable();
 
+        // SEARCH
         if (!string.IsNullOrWhiteSpace(search))
         {
+            search = search.Trim();
+
             query = query.Where(r =>
-                r.Title.ToLower().Contains(search.ToLower()));
+                r.Title.ToLower().Contains(search.ToLower()) ||
+                r.Genre.ToLower().Contains(search.ToLower()) ||
+                r.ReleaseArtists.Any(ra =>
+                    ra.Artist.ArtistName.ToLower()
+                        .Contains(search.ToLower())));
         }
 
+        // GENRE
         if (!string.IsNullOrWhiteSpace(genre))
         {
@@ -35,10 +54,66 @@
         }
 
-        var releases = query
-            .OrderBy(r => r.Title)
-            .ToList();
+        // FORMAT
+        if (!string.IsNullOrWhiteSpace(format))
+        {
+            query = query.Where(r =>
+                r.Products.Any(p =>
+                    p.Format.ToString() == format));
+        }
+
+        // TYPE
+        if (!string.IsNullOrWhiteSpace(type))
+        {
+            if (type.Equals("Album", StringComparison.OrdinalIgnoreCase))
+            {
+                query = query.Where(r => r.Album != null);
+            }
+            else if (type.Equals("Single", StringComparison.OrdinalIgnoreCase))
+            {
+                query = query.Where(r => r.SingleRelease != null);
+            }
+        }
+
+        // SORT
+        query = sort switch
+        {
+            "titleDesc" =>
+                query.OrderByDescending(r => r.Title),
+
+            "newest" =>
+                query.OrderByDescending(r => r.ReleaseDate),
+
+            "oldest" =>
+                query.OrderBy(r => r.ReleaseDate),
+
+            "format" =>
+                query.OrderBy(r =>
+                    r.Products
+                        .Select(p => p.Format.ToString())
+                        .FirstOrDefault()),
+
+            "priceAsc" =>
+                query.OrderBy(r =>
+                    r.Products
+                        .Select(p => (decimal?)p.Price)
+                        .Min()),
+
+            "priceDesc" =>
+                query.OrderByDescending(r =>
+                    r.Products
+                        .Select(p => (decimal?)p.Price)
+                        .Max()),
+
+            _ =>
+                query.OrderBy(r => r.Title)
+        };
+
+        var releases = query.ToList();
 
         ViewBag.Search = search;
         ViewBag.Genre = genre;
+        ViewBag.Format = format;
+        ViewBag.Type = type;
+        ViewBag.Sort = sort;
 
         ViewBag.Genres = _context.Releases
@@ -48,21 +123,33 @@
             .ToList();
 
+        ViewBag.Formats = Enum
+            .GetValues<ProductFormat>()
+            .Select(x => x.ToString())
+            .ToList();
+
         return View(releases);
     }
 
 
-    // Release details
+    // =========================================================
+    // DETAILS
+    // =========================================================
+
     public IActionResult Details(long id)
     {
         var release = _context.Releases
             .Include(r => r.Products)
+
             .Include(r => r.ReleaseArtists)
-            .ThenInclude(ra => ra.Artist)
+                .ThenInclude(ra => ra.Artist)
+
             .Include(r => r.Album)
-            .ThenInclude(a => a!.AlbumSongs)
-            .ThenInclude(x => x.Song)
+                .ThenInclude(a => a!.AlbumSongs)
+                    .ThenInclude(x => x.Song)
+                        .ThenInclude(s => s.SongArtists)
+                            .ThenInclude(sa => sa.Artist)
+
             .Include(r => r.SingleRelease)
-            .ThenInclude(s => s!.SingleFeatures)
-            .ThenInclude(x => x.Song)
+
             .FirstOrDefault(r => r.ReleaseId == id);
 
Index: KernelRecordsMVC.Web/Controllers/SaleController.cs
===================================================================
--- KernelRecordsMVC.Web/Controllers/SaleController.cs	(revision fa0fbaf2cdd288351eeaf3d4d17d8a6158ea5adc)
+++ KernelRecordsMVC.Web/Controllers/SaleController.cs	(revision fa0fbaf2cdd288351eeaf3d4d17d8a6158ea5adc)
@@ -0,0 +1,125 @@
+﻿using KernelRecordsMVC.Application.ViewModels;
+using KernelRecordsMVC.Domain.Enums;
+using KernelRecordsMVC.Infrastructure.Data;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+
+namespace KernelRecordsMVC.Web.Controllers;
+
+public class SaleController : Controller
+{
+    private readonly KernelRecordsContext _context;
+
+    public SaleController(KernelRecordsContext context)
+    {
+        _context = context;
+    }
+
+    public IActionResult Index()
+    {
+        var products = _context.Products
+            .Include(p => p.Release)
+                .ThenInclude(r => r.ReleaseArtists)
+                    .ThenInclude(ra => ra.Artist)
+            .Include(p => p.ModificationProducts)
+                .ThenInclude(mp => mp.Modification)
+            .ToList();
+
+
+        var result = new List<SaleItemViewModel>();
+
+
+        foreach (var product in products)
+        {
+            // Get the latest modification for this product.
+            var latestModification = product.ModificationProducts
+                .Select(x => x.Modification)
+                .OrderByDescending(m => m.DateModified)
+                .ThenByDescending(m => m.ModificationId)
+                .FirstOrDefault();
+
+
+            // Product is only considered currently on sale
+            // if its latest modification is a discount.
+            if (latestModification == null ||
+                latestModification.TypeOfModification != ModificationType.DISCOUNT ||
+                !latestModification.Discount.HasValue ||
+                latestModification.Discount.Value <= 0)
+            {
+                continue;
+            }
+
+
+            var percentage =
+                latestModification.Discount.Value;
+
+
+            // IMPORTANT:
+            // Product.Price already contains the discounted price.
+            var salePrice =
+                product.Price;
+
+
+            // Reconstruct the price before the discount
+            // so it can be displayed crossed out.
+            var originalPrice =
+                percentage > 0 &&
+                percentage < 100
+                    ? Math.Round(
+                        salePrice /
+                        (1m - percentage / 100m),
+                        2,
+                        MidpointRounding.AwayFromZero)
+                    : salePrice;
+
+
+            result.Add(new SaleItemViewModel
+            {
+                ProductId =
+                    product.ProductId,
+
+                ReleaseId =
+                    product.ReleaseId,
+
+                ReleaseTitle =
+                    product.Release.Title,
+
+                CoverPhoto =
+                    product.Release.CoverPhoto,
+
+                Format =
+                    product.Format.ToString(),
+
+                OriginalPrice =
+                    originalPrice,
+
+                SalePrice =
+                    salePrice,
+
+                DiscountPercentage =
+                    percentage,
+
+                Stock =
+                    product.Stock,
+
+                ArtistName =
+                    string.Join(
+                        ", ",
+                        product.Release.ReleaseArtists
+                            .OrderBy(x => x.ReleaseOrdinal)
+                            .Select(x => x.Artist.ArtistName))
+            });
+        }
+
+
+        result = result
+            .OrderByDescending(x =>
+                x.DiscountPercentage)
+            .ThenBy(x =>
+                x.ReleaseTitle)
+            .ToList();
+
+
+        return View(result);
+    }
+}
Index: KernelRecordsMVC.Web/Controllers/TopSellersController.cs
===================================================================
--- KernelRecordsMVC.Web/Controllers/TopSellersController.cs	(revision fa0fbaf2cdd288351eeaf3d4d17d8a6158ea5adc)
+++ KernelRecordsMVC.Web/Controllers/TopSellersController.cs	(revision fa0fbaf2cdd288351eeaf3d4d17d8a6158ea5adc)
@@ -0,0 +1,79 @@
+﻿using KernelRecordsMVC.Application.ViewModels;
+using KernelRecordsMVC.Domain.Enums;
+using KernelRecordsMVC.Infrastructure.Data;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+
+namespace KernelRecordsMVC.Web.Controllers;
+
+public class TopSellersController : Controller
+{
+    private readonly KernelRecordsContext _context;
+
+    public TopSellersController(KernelRecordsContext context)
+    {
+        _context = context;
+    }
+
+    public IActionResult Index()
+    {
+        var completedStatuses = new[]
+        {
+            OrderStatusType.PAID,
+            OrderStatusType.SHIPPED,
+            OrderStatusType.DELIVERED
+        };
+
+        var sellers = _context.OrderProducts
+            .Where(op =>
+                completedStatuses.Contains(op.Order.Status))
+            .GroupBy(op => op.ProductId)
+            .Select(g => new
+            {
+                ProductId = g.Key,
+                QuantitySold = g.Sum(x => x.Quantity)
+            })
+            .OrderByDescending(x => x.QuantitySold)
+            .Take(20)
+            .ToList();
+
+        var productIds = sellers
+            .Select(x => x.ProductId)
+            .ToList();
+
+        var products = _context.Products
+            .Include(p => p.Release)
+                .ThenInclude(r => r.ReleaseArtists)
+                    .ThenInclude(ra => ra.Artist)
+            .Where(p => productIds.Contains(p.ProductId))
+            .ToList();
+
+        var result = sellers
+            .Select(s =>
+            {
+                var product = products
+                    .First(p => p.ProductId == s.ProductId);
+
+                return new TopSellerViewModel
+                {
+                    ProductId = product.ProductId,
+                    ReleaseId = product.ReleaseId,
+                    ReleaseTitle = product.Release.Title,
+                    CoverPhoto = product.Release.CoverPhoto,
+                    Format = product.Format.ToString(),
+                    Price = product.Price,
+                    Stock = product.Stock,
+                    SoldQuantity = s.QuantitySold,
+
+                    ArtistName = string.Join(
+                        ", ",
+                        product.Release.ReleaseArtists
+                            .OrderBy(x => x.ReleaseOrdinal)
+                            .Select(x => x.Artist.ArtistName))
+                };
+            })
+            .ToList();
+
+        return View(result);
+    }
+}
Index: KernelRecordsMVC.Web/Controllers/WishlistController.cs
===================================================================
--- KernelRecordsMVC.Web/Controllers/WishlistController.cs	(revision fa0fbaf2cdd288351eeaf3d4d17d8a6158ea5adc)
+++ KernelRecordsMVC.Web/Controllers/WishlistController.cs	(revision fa0fbaf2cdd288351eeaf3d4d17d8a6158ea5adc)
@@ -0,0 +1,228 @@
+﻿using KernelRecordsMVC.Infrastructure.Data;
+using KernelRecordsMVC.Models;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+
+namespace KernelRecordsMVC.Web.Controllers;
+
+public class WishlistController : Controller
+{
+    private readonly KernelRecordsContext _context;
+
+    public WishlistController(KernelRecordsContext context)
+    {
+        _context = context;
+    }
+
+    // ==========================================
+    // WISHLIST
+    // ==========================================
+
+    [HttpGet]
+    public IActionResult Index()
+    {
+        var userId = GetUserId();
+
+        if (userId == null)
+        {
+            return RedirectToAction(
+                "Login",
+                "Account");
+        }
+
+        var wishlist = _context.Wishlists
+            .Include(w => w.WishlistProducts)
+                .ThenInclude(wp => wp.Product)
+                    .ThenInclude(p => p.Release)
+            .FirstOrDefault(w => w.UserId == userId.Value);
+
+        if (wishlist == null)
+        {
+            return View(new Wishlist());
+        }
+
+        return View(wishlist);
+    }
+
+
+    // ==========================================
+    // ADD TO WISHLIST
+    // ==========================================
+
+    [HttpPost]
+    [ValidateAntiForgeryToken]
+    public IActionResult Add(long productId)
+    {
+        var userId = GetUserId();
+
+        if (userId == null)
+        {
+            return RedirectToAction(
+                "Login",
+                "Account");
+        }
+
+        var product = _context.Products
+            .Include(p => p.Release)
+            .FirstOrDefault(p => p.ProductId == productId);
+
+        if (product == null)
+            return NotFound();
+
+
+        // Find existing wishlist
+        var wishlist = _context.Wishlists
+            .Include(w => w.WishlistProducts)
+            .FirstOrDefault(w =>
+                w.UserId == userId.Value);
+
+
+        // Create wishlist if user doesn't have one
+        if (wishlist == null)
+        {
+            wishlist = new Wishlist
+            {
+                WishlistId = GetNextWishlistId(),
+                UserId = userId.Value
+            };
+
+            _context.Wishlists.Add(wishlist);
+            _context.SaveChanges();
+        }
+
+
+        // Check if product is already there
+        var alreadyExists = wishlist.WishlistProducts
+            .Any(x => x.ProductId == productId);
+
+        if (!alreadyExists)
+        {
+            var wishlistProduct = new WishlistProduct
+            {
+                WishlistId = wishlist.WishlistId,
+                ProductId = productId,
+                AddedAt = DateTime.Today
+            };
+
+            _context.WishlistProducts.Add(wishlistProduct);
+            _context.SaveChanges();
+
+            TempData["Success"] =
+                "Product added to your wishlist.";
+        }
+        else
+        {
+            TempData["Info"] =
+                "This product is already in your wishlist.";
+        }
+
+        return RedirectToAction(
+            "Details",
+            "Release",
+            new { id = product.ReleaseId });
+    }
+
+
+    // ==========================================
+    // REMOVE FROM WISHLIST
+    // ==========================================
+
+    [HttpPost]
+    [ValidateAntiForgeryToken]
+    public IActionResult Remove(long productId)
+    {
+        var userId = GetUserId();
+
+        if (userId == null)
+        {
+            return RedirectToAction(
+                "Login",
+                "Account");
+        }
+
+        var item = _context.WishlistProducts
+            .Include(x => x.Wishlist)
+            .FirstOrDefault(x =>
+                x.ProductId == productId &&
+                x.Wishlist.UserId == userId.Value);
+
+        if (item != null)
+        {
+            _context.WishlistProducts.Remove(item);
+            _context.SaveChanges();
+
+            TempData["Success"] =
+                "Product removed from your wishlist.";
+        }
+
+        return RedirectToAction(nameof(Index));
+    }
+
+
+    // ==========================================
+    // MOVE WISHLIST PRODUCT TO CART
+    // ==========================================
+
+    [HttpPost]
+    [ValidateAntiForgeryToken]
+    public IActionResult AddToCart(long productId)
+    {
+        var userId = GetUserId();
+
+        if (userId == null)
+        {
+            return RedirectToAction(
+                "Login",
+                "Account");
+        }
+
+        var product = _context.Products
+            .Include(p => p.Release)
+            .FirstOrDefault(p => p.ProductId == productId);
+
+        if (product == null)
+            return NotFound();
+
+        if (product.Stock <= 0)
+        {
+            TempData["Error"] =
+                "This product is currently out of stock.";
+
+            return RedirectToAction(nameof(Index));
+        }
+
+        return RedirectToAction(
+            "AddProduct",
+            "Order",
+            new { id = productId });
+    }
+
+
+    // ==========================================
+    // USER ID
+    // ==========================================
+
+    private long? GetUserId()
+    {
+        var userId = HttpContext.Session.GetInt32("UserId");
+
+        if (userId.HasValue)
+            return userId.Value;
+
+        return null;
+    }
+
+
+    // ==========================================
+    // WISHLIST ID
+    // ==========================================
+
+    private long GetNextWishlistId()
+    {
+        var maxId = _context.Wishlists
+            .Select(x => (long?)x.WishlistId)
+            .Max();
+
+        return (maxId ?? 0) + 1;
+    }
+}
