Index: KernelRecordsMVC.Web/Controllers/AccountController.cs
===================================================================
--- KernelRecordsMVC.Web/Controllers/AccountController.cs	(revision 08aefc6f3b51f4bfcb7471a2fd319c064171b1c5)
+++ KernelRecordsMVC.Web/Controllers/AccountController.cs	(revision 08aefc6f3b51f4bfcb7471a2fd319c064171b1c5)
@@ -0,0 +1,175 @@
+﻿using Microsoft.AspNetCore.Mvc;
+using KernelRecordsMVC.Data;
+using KernelRecordsMVC.Models;
+using KernelRecordsMVC.Application.ViewModels;
+using Microsoft.AspNetCore.Http;
+
+namespace KernelRecordsMVC.Controllers;
+
+public class AccountController : Controller
+{
+    private readonly KernelRecordsContext _context;
+
+    public AccountController(KernelRecordsContext context)
+    {
+        _context = context;
+    }
+
+    // ==============================
+    // REGISTER - GET
+    // ==============================
+
+    [HttpGet]
+    public IActionResult Register()
+    {
+        return View();
+    }
+
+
+    // ==============================
+    // REGISTER - POST
+    // ==============================
+
+    [HttpPost]
+    [ValidateAntiForgeryToken]
+    public IActionResult Register(RegisterViewModel model)
+    {
+        if (!ModelState.IsValid)
+            return View(model);
+
+        if (_context.Users.Any(x => x.Username == model.Username))
+        {
+            ModelState.AddModelError(
+                "Username",
+                "Username is already taken.");
+
+            return View(model);
+        }
+
+        if (_context.Users.Any(x => x.Email == model.Email))
+        {
+            ModelState.AddModelError(
+                "Email",
+                "Email is already registered.");
+
+            return View(model);
+        }
+
+        var user = new User
+        {
+            Email = model.Email,
+            Username = model.Username,
+
+            // We'll replace this with proper password hashing
+            // in the security cleanup section.
+            Password = model.Password,
+
+            DateCreated = DateTime.Today,
+            ShippingAddress = model.ShippingAddress,
+            TelephoneNumber = model.TelephoneNumber
+        };
+
+        _context.Users.Add(user);
+
+        _context.SaveChanges();
+
+        // Every normal registered user is a Consumer.
+        var consumer = new Consumer
+        {
+            UserId = user.UserId,
+            PointsCollected = 0
+        };
+
+        _context.Consumers.Add(consumer);
+
+        _context.SaveChanges();
+
+        return RedirectToAction(nameof(Login));
+    }
+
+
+    // ==============================
+    // LOGIN - GET
+    // ==============================
+
+    [HttpGet]
+    public IActionResult Login()
+    {
+        return View();
+    }
+
+
+    // ==============================
+    // LOGIN - POST
+    // ==============================
+
+    [HttpPost]
+    [ValidateAntiForgeryToken]
+    public IActionResult Login(LoginViewModel model)
+    {
+        if (!ModelState.IsValid)
+            return View(model);
+
+        var user = _context.Users
+            .FirstOrDefault(x =>
+                x.Username == model.Username &&
+                x.Password == model.Password);
+
+        if (user == null)
+        {
+            ModelState.AddModelError(
+                "",
+                "Invalid username or password.");
+
+            return View(model);
+        }
+
+        HttpContext.Session.SetInt32(
+            "UserId",
+            checked((int)user.UserId));
+
+        HttpContext.Session.SetString(
+            "Username",
+            user.Username);
+
+        // Determine account type from the actual
+        // ADMINS / CONSUMERS tables.
+        var admin = _context.Admins
+            .FirstOrDefault(x => x.UserId == user.UserId);
+
+        if (admin != null)
+        {
+            HttpContext.Session.SetString(
+                "Role",
+                "Admin");
+
+            HttpContext.Session.SetString(
+                "AdminType",
+                admin.Type.ToString());
+        }
+        else
+        {
+            HttpContext.Session.SetString(
+                "Role",
+                "Consumer");
+        }
+
+        return RedirectToAction(
+            "Index",
+            "Home");
+    }
+
+
+    // ==============================
+    // LOGOUT
+    // ==============================
+
+    public IActionResult Logout()
+    {
+        HttpContext.Session.Clear();
+
+        return RedirectToAction(
+            "Index",
+            "Home");
+    }
+}
Index: KernelRecordsMVC.Web/Controllers/AdminController.cs
===================================================================
--- KernelRecordsMVC.Web/Controllers/AdminController.cs	(revision 08aefc6f3b51f4bfcb7471a2fd319c064171b1c5)
+++ KernelRecordsMVC.Web/Controllers/AdminController.cs	(revision 08aefc6f3b51f4bfcb7471a2fd319c064171b1c5)
@@ -0,0 +1,388 @@
+﻿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;
+
+public class AdminController : Controller
+{
+    private readonly KernelRecordsContext _context;
+
+    public AdminController(KernelRecordsContext context)
+    {
+        _context = context;
+    }
+
+
+    // ==========================================
+    // ADMIN DASHBOARD
+    // ==========================================
+
+    [HttpGet]
+    public IActionResult Index()
+    {
+        if (!IsAdmin())
+            return Forbid();
+
+        return View();
+    }
+
+
+    // ==========================================
+    // UC008
+    // NEW PRODUCT - GET
+    // ==========================================
+
+    [HttpGet]
+    public IActionResult CreateProduct()
+    {
+        if (!IsProductManager())
+            return Forbid();
+
+        ViewBag.Releases = _context.Releases
+            .OrderBy(x => x.Title)
+            .ToList();
+
+        return View(new CreateProductViewModel());
+    }
+
+
+    // ==========================================
+    // UC008
+    // NEW PRODUCT - POST
+    // ==========================================
+
+    [HttpPost]
+    [ValidateAntiForgeryToken]
+    public IActionResult CreateProduct(
+        CreateProductViewModel model)
+    {
+        if (!IsProductManager())
+            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",
+                "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);
+
+        if (alreadyExists)
+        {
+            ModelState.AddModelError(
+                "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
+    // ==========================================
+
+    private bool IsAdmin()
+    {
+        return HttpContext.Session
+            .GetString("Role") == "Admin";
+    }
+
+
+    private bool IsProductManager()
+    {
+        var role = HttpContext.Session
+            .GetString("Role");
+
+        var adminType = HttpContext.Session
+            .GetString("AdminType");
+
+        return role == "Admin" &&
+               (adminType == "PRODUCT_MANAGER" ||
+                adminType == "SUPER_ADMIN");
+    }
+
+
+    private long? GetCurrentUserId()
+    {
+        var value = HttpContext.Session
+            .GetString("UserId");
+
+        if (long.TryParse(value, out var userId))
+            return userId;
+
+        return null;
+    }
+
+
+    private long GetNextProductId()
+    {
+        var maxId = _context.Products
+            .Select(x => (long?)x.ProductId)
+            .Max();
+
+        return (maxId ?? 0) + 1;
+    }
+
+
+    private void LoadReleases()
+    {
+        ViewBag.Releases = _context.Releases
+            .OrderBy(x => x.Title)
+            .ToList();
+    }
+
+
+    private void CreateModification(
+        ModificationType type,
+        long productId,
+        decimal? discount = null)
+    {
+        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);
+
+        _context.SaveChanges();
+
+
+        var modificationProduct =
+            new ModificationProduct
+            {
+                ModificationId =
+                    modification.ModificationId,
+
+                ProductId = productId
+            };
+
+
+        _context.ModificationProducts.Add(
+            modificationProduct);
+
+        _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 08aefc6f3b51f4bfcb7471a2fd319c064171b1c5)
@@ -0,0 +1,32 @@
+using System.Diagnostics;
+using Microsoft.AspNetCore.Mvc;
+using KernelRecordsMVC.Models;
+using Microsoft.Extensions.Logging;
+
+namespace KernelRecordsMVC.Controllers;
+
+public class HomeController : Controller
+{
+    private readonly ILogger<HomeController> _logger;
+
+    public HomeController(ILogger<HomeController> logger)
+    {
+        _logger = logger;
+    }
+
+    public IActionResult Index()
+    {
+        return View();
+    }
+
+    public IActionResult Privacy()
+    {
+        return View();
+    }
+
+    [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
+    public IActionResult Error()
+    {
+        return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
+    }
+}
Index: KernelRecordsMVC.Web/Controllers/OrderController.cs
===================================================================
--- KernelRecordsMVC.Web/Controllers/OrderController.cs	(revision 08aefc6f3b51f4bfcb7471a2fd319c064171b1c5)
+++ KernelRecordsMVC.Web/Controllers/OrderController.cs	(revision 08aefc6f3b51f4bfcb7471a2fd319c064171b1c5)
@@ -0,0 +1,318 @@
+﻿using Microsoft.AspNetCore.Http;
+
+namespace KernelRecordsMVC.Controllers;
+
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using KernelRecordsMVC.Data;
+using KernelRecordsMVC.Models;
+using KernelRecordsMVC.Domain.Enums;
+using KernelRecordsMVC.Application.ViewModels;
+
+public class OrderController : Controller
+{
+    private readonly KernelRecordsContext _context;
+
+    public OrderController(KernelRecordsContext context)
+    {
+        _context = context;
+    }
+
+
+    // ==========================================
+    // ADD PRODUCT TO ORDER
+    // UC007
+    // ==========================================
+
+    [HttpGet]
+    public IActionResult AddProduct(long id)
+    {
+        var userId = GetUserId();
+
+        if (userId == null)
+            return RedirectToAction(
+                "Login",
+                "Account");
+
+        var product = _context.Products
+            .Include(p => p.Release)
+            .FirstOrDefault(p => p.ProductId == id);
+
+        if (product == null)
+            return NotFound();
+
+        if (product.Stock <= 0)
+        {
+            TempData["Error"] =
+                "This product is currently out of stock.";
+
+            return RedirectToAction(
+                "Details",
+                "Release",
+                new { id = product.ReleaseId });
+        }
+
+
+        // Find an existing pending order.
+        var order = _context.Orders
+            .Include(o => o.OrderProducts)
+            .FirstOrDefault(o =>
+                o.UserId == userId.Value &&
+                o.Status == OrderStatusType.PENDING);
+
+
+        // If there is no pending order, create one.
+        if (order == null)
+        {
+            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
+            };
+
+            _context.Orders.Add(order);
+
+            _context.SaveChanges();
+        }
+
+
+        // Check whether product is already in cart.
+        var existingItem = order.OrderProducts
+            .FirstOrDefault(x =>
+                x.ProductId == product.ProductId);
+
+
+        if (existingItem != null)
+        {
+            if (existingItem.Quantity + 1 >
+                product.Stock)
+            {
+                TempData["Error"] =
+                    "There is not enough stock available.";
+
+                return RedirectToAction(
+                    "Details",
+                    "Release",
+                    new { id = product.ReleaseId });
+            }
+
+            existingItem.Quantity++;
+        }
+        else
+        {
+            var orderProduct = new OrderProduct
+            {
+                OrderId = order.OrderId,
+
+                ProductId = product.ProductId,
+
+                PriceAtPurchase = product.Price,
+
+                Quantity = 1
+            };
+
+            _context.OrderProducts.Add(orderProduct);
+        }
+
+        _context.SaveChanges();
+
+
+        return RedirectToAction(nameof(Cart));
+    }
+
+
+    // ==========================================
+    // CART
+    // ==========================================
+
+    [HttpGet]
+    public IActionResult Cart()
+    {
+        var userId = GetUserId();
+
+        if (userId == null)
+        {
+            return RedirectToAction(
+                "Login",
+                "Account");
+        }
+
+
+        var order = _context.Orders
+            .Include(o => o.OrderProducts)
+                .ThenInclude(op => op.Product)
+                    .ThenInclude(p => p.Release)
+            .FirstOrDefault(o =>
+                o.UserId == userId.Value &&
+                o.Status == OrderStatusType.PENDING);
+
+
+        var viewModel = new CartViewModel();
+
+
+        if (order != null)
+        {
+            viewModel.Items = order.OrderProducts
+                .Select(op => new CartItemViewModel
+                {
+                    ProductId = op.ProductId,
+
+                    ReleaseId =
+                        op.Product.ReleaseId,
+
+                    ReleaseTitle =
+                        op.Product.Release.Title,
+
+                    Format =
+                        op.Product.Format.ToString(),
+
+                    Price =
+                        op.PriceAtPurchase,
+
+                    Quantity =
+                        op.Quantity,
+
+                    Stock =
+                        op.Product.Stock
+                })
+                .ToList();
+        }
+
+
+        return View(viewModel);
+    }
+
+
+    // ==========================================
+    // REMOVE PRODUCT
+    // ==========================================
+
+    [HttpPost]
+    [ValidateAntiForgeryToken]
+    public IActionResult RemoveProduct(long id)
+    {
+        var userId = GetUserId();
+
+        if (userId == null)
+            return RedirectToAction(
+                "Login",
+                "Account");
+
+
+        var item = _context.OrderProducts
+            .Include(x => x.Order)
+            .FirstOrDefault(x =>
+                x.ProductId == id &&
+                x.Order.UserId == userId.Value &&
+                x.Order.Status ==
+                    OrderStatusType.PENDING);
+
+
+        if (item != null)
+        {
+            _context.OrderProducts.Remove(item);
+            _context.SaveChanges();
+        }
+
+
+        return RedirectToAction(nameof(Cart));
+    }
+
+
+    // ==========================================
+    // UPDATE QUANTITY
+    // ==========================================
+
+    [HttpPost]
+    [ValidateAntiForgeryToken]
+    public IActionResult UpdateQuantity(
+        long id,
+        long quantity)
+    {
+        var userId = GetUserId();
+
+        if (userId == null)
+            return RedirectToAction(
+                "Login",
+                "Account");
+
+
+        if (quantity <= 0)
+        {
+            return RemoveProduct(id);
+        }
+
+
+        var item = _context.OrderProducts
+            .Include(x => x.Order)
+            .Include(x => x.Product)
+            .FirstOrDefault(x =>
+                x.ProductId == id &&
+                x.Order.UserId == userId.Value &&
+                x.Order.Status ==
+                    OrderStatusType.PENDING);
+
+
+        if (item == null)
+            return NotFound();
+
+
+        if (quantity > item.Product.Stock)
+        {
+            TempData["Error"] =
+                "The requested quantity exceeds available stock.";
+
+            return RedirectToAction(nameof(Cart));
+        }
+
+
+        item.Quantity = quantity;
+
+        _context.SaveChanges();
+
+
+        return RedirectToAction(nameof(Cart));
+    }
+
+
+    // ==========================================
+    // USER ID
+    // ==========================================
+
+    private long? GetUserId()
+    {
+        var value =
+            HttpContext.Session.GetString("UserId");
+
+        if (long.TryParse(value, out var userId))
+            return userId;
+
+        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 08aefc6f3b51f4bfcb7471a2fd319c064171b1c5)
@@ -0,0 +1,74 @@
+﻿using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using KernelRecordsMVC.Data;
+
+namespace KernelRecordsMVC.Controllers;
+
+public class ReleaseController : Controller
+{
+    private readonly KernelRecordsContext _context;
+
+    public ReleaseController(KernelRecordsContext context)
+    {
+        _context = context;
+    }
+
+    // UC003 - Browse Releases
+    public IActionResult Index(string? search, string? genre)
+    {
+        var query = _context.Releases
+            .Include(r => r.Products)
+            .Include(r => r.ReleaseArtists)
+            .ThenInclude(ra => ra.Artist)
+            .AsQueryable();
+
+        if (!string.IsNullOrWhiteSpace(search))
+        {
+            query = query.Where(r =>
+                r.Title.ToLower().Contains(search.ToLower()));
+        }
+
+        if (!string.IsNullOrWhiteSpace(genre))
+        {
+            query = query.Where(r =>
+                r.Genre.ToLower() == genre.ToLower());
+        }
+
+        var releases = query
+            .OrderBy(r => r.Title)
+            .ToList();
+
+        ViewBag.Search = search;
+        ViewBag.Genre = genre;
+
+        ViewBag.Genres = _context.Releases
+            .Select(r => r.Genre)
+            .Distinct()
+            .OrderBy(g => g)
+            .ToList();
+
+        return View(releases);
+    }
+
+
+    // Release details
+    public IActionResult Details(long id)
+    {
+        var release = _context.Releases
+            .Include(r => r.Products)
+            .Include(r => r.ReleaseArtists)
+            .ThenInclude(ra => ra.Artist)
+            .Include(r => r.Album)
+            .ThenInclude(a => a!.AlbumSongs)
+            .ThenInclude(x => x.Song)
+            .Include(r => r.SingleRelease)
+            .ThenInclude(s => s!.SingleFeatures)
+            .ThenInclude(x => x.Song)
+            .FirstOrDefault(r => r.ReleaseId == id);
+
+        if (release == null)
+            return NotFound();
+
+        return View(release);
+    }
+}
