Real-time Ecommerce Application using ASP.NET Core MVC

Real-time Ecommerce Application using ASP.NET Core MVC

Let us develop a Real-time Ecommerce Application using ASP.NET Core MVC and Entity Framework Core (EF Core) using SQL Server Database. We will build a robust real-time Ecommerce application using ASP.NET Core MVC that manages customer orders and generates invoices dynamically. The system supports both customer-facing order and payment flows and admin functionalities for managing invoice templates. 

User Authentication & Authorization:
  • Customer Registration and Login: The application provides a basic cookie-based authentication mechanism. Customers can register, log in, and log out with their credentials stored in the database.
  • Admin Access: Admin-specific pages (such as the template management screens) are accessible only to authorized users (using role-based checks or specific claims).
Customer Order and Payment Flow:
  • Order Creation: Customers can browse available products, adjust quantities via a dynamic interface, and place orders. The system calculates the total order amount in real time and stores only valid order items.
  • Payment Processing: Once an order is placed, customers proceed to a payment page where they select a payment method (Credit Card, PayPal, or COD). The application simulates payment processing, updates the order status (Completed, Cancelled, or Pending), and generates an invoice based on the order details.
  • Invoice Generation: Invoices are generated as PDF documents using IronPdf. The HTML invoice templates contain placeholders (such as {CustomerName}, {OrderDate}, etc.) that are dynamically replaced with actual order data when generating the final invoice.
  • Order Confirmation: After processing payment, customers are redirected to an order confirmation page that displays the complete order summary and details. They also have the option to download or view the invoice.
Admin Management of Invoice Templates:
  • CRUD Operations: Admin users can manage invoice templates via a dedicated CRUD module. They can create, edit, and delete invoice templates using a form that integrates CKEditor for rich HTML content editing.
  • Template Customization: The invoice templates allow placeholders to insert dynamic order data. A side panel displays a list of allowed placeholders and descriptions, ensuring that admins use the correct format for generating invoices.

Let’s first see the application flow and the different pages we will develop as part of this Real-time e-commerce application.

Customer Registration Page:

Real-time Ecommerce Application using ASP.NET Core MVC

Customer and Admin Login Page:

Real-time Ecommerce Application using ASP.NET Core MVC

Customer Order List Page:

Customer Order List Page

Place Order Page:

Place Order Page

Order Payment Page:

Order Payment Page

Order Confirmation, Failed, Pending Page:

Order Confirmation, Failed, Pending Page

Order Confirmation Email and Order Invoice Attachment:

Order Confirmation Email and Order Invoice Attachment

Manage Invoice Template Page: (Only for Admin user)

Manage Invoice Template Page

Order Create Invoice Template Page:

Order Create Invoice Template Page

Order Edit Invoice Template Page:

Real-time Ecommerce Application using ASP.NET Core MVC

Order Delete Invoice Template Page:

Real-time Ecommerce Application using ASP.NET Core MVC

Technology Stack:
  • ASP.NET Core MVC: Provides a clean separation of concerns with Controllers, Views, and Models.
  • Entity Framework Core: Manages data access and maps domain models to an SQL server database.
  • SQL Server: The backend database that stores and manages the data.
  • IronPdf: Generates high-fidelity PDF invoices from HTML templates.
  • CKEditor 5: Enables rich text editing for invoice templates.
  • Bootstrap: Ensures a responsive and professional user interface across devices.
Create a New ASP.NET Core MVC Project and Install Packages

Open Visual Studio and create a new ASP.NET Core Web App (Model-View-Controller) project. Name the project OrderInvoiceSystem. We will use the IronPdf package to generate PDF from HTML. So, please install the necessary NuGet packages using the Package Manager Console:

  • Install-Package Microsoft.EntityFrameworkCore.SqlServer
  • Install-Package Microsoft.EntityFrameworkCore.Tools
  • Install-Package IronPdf
Creating Models

Models represent the core domain entities; each corresponds to a table in the database. They hold the data and relationships for the business domain. If a folder named Models does not exist, please create one in the project root directory.

TemplateType Enum

Create a file named TemplateType.cs in the Models folder, and then copy and paste the following code. This enumeration defines the types of invoice templates available in the system. For example, you can have templates for order completion, cancellation, or pending orders. This helps the application decide which template to use based on an order’s status.

namespace OrderInvoiceSystem.Models
{
    public enum TemplateType
    {
        OrderCompletion = 1,
        OrderCancelled = 2,
        OrderPending = 3,
        PaymentSucess = 4,
        PaymentFailed = 5,
        PaymentPending = 6,
        CustomerRegistration = 7
    }
}
Template Model

Create a file named Template.cs in the Models folder, and then copy and paste the following code. This model holds the HTML template data managed by an admin. It includes properties for the template name, HTML content with placeholders (e.g., {CustomerName}, {OrderDate}, etc.), template type (using the enum above), and metadata such as creation and update information.

using System.ComponentModel.DataAnnotations;
namespace OrderInvoiceSystem.Models
{
    public class Template
    {
        [Key]
        public int Id { get; set; }

        [Required, MaxLength(200)]
        public string TemplateName { get; set; }

        // HTML content with placeholders (e.g., {CustomerName}, {OrderDate}, {ProductList}, {TotalAmount})
        [Required]
        public string HtmlContent { get; set; }

        // The type of invoice template.
        public TemplateType TemplateType { get; set; }

        public DateTime CreatedDate { get; set; }
        public string CreatedBy { get; set; }
        public DateTime? UpdatedDate { get; set; }
        public string? UpdatedBy { get; set; }
    }
}
Customer Model

Create a file named Customer.cs in the Models folder, and then copy and paste the following code. This Model represents a customer who registers and places orders. It includes basic properties such as the customer’s name, email, password (note: plain text for simplicity; production apps should hash passwords), phone number, billing address, and a list of orders associated with the customer.

namespace OrderInvoiceSystem.Models
{
    public class Customer
    {
        public int Id { get; set; }
        public string CustomerName { get; set; }
        public string Email { get; set; }
        // For simplicity, we store password in plain text here.
        // In production, use proper password hashing.
        public string Password { get; set; }
        public string Phone { get; set; }
        public string BillingAddress { get; set; }
        public List<Order> Orders { get; set; }
    }
}
Product Model

Create a file named Product.cs in the Models folder, and then copy and paste the following code. This model represents an item available for purchase. It contains details like the product name, category, optional description, and unit price. It also has a navigation property (a list of order items) that links this product to orders.

using System.ComponentModel.DataAnnotations.Schema;
namespace OrderInvoiceSystem.Models
{
    public class Product
    {
        public int Id { get; set; }
        public string ProductName { get; set; }
        public string Category { get; set; }
        public string? Description { get; set; }

        [Column(TypeName = "decimal(18,2)")]
        public decimal UnitPrice { get; set; }
        public List<OrderItem> Items { get; set; }
    }
}
Order Model

Create a file named Order.cs in the Models folder, and then copy and paste the following code. This Model represents an order placed by a customer. It holds the order number, date, status (e.g., Pending, Completed, or Canceled), total amount, and navigation properties for the associated customer, the collection of order items, and a one-to-one relationship with a Payment.

using System.ComponentModel.DataAnnotations.Schema;
namespace OrderInvoiceSystem.Models
{
    public class Order
    {
        public int Id { get; set; }

        // Foreign key and navigation property for Customer
        public int CustomerId { get; set; }
        public Customer Customer { get; set; }
        public string OrderNumber { get; set; }
        public DateTime OrderDate { get; set; }

        // Collection of OrderItems
        // Navigation property: an order can have multiple order items.
        public ICollection<OrderItem> OrderItems { get; set; }

        [Column(TypeName = "decimal(18,2)")]
        public decimal TotalAmount { get; set; }

        // e.g., Pending, Completed, Canceled 
        public string OrderStatus { get; set; }

        public DateTime CreatedDate { get; set; }

        // Navigation property for Payment (one-to-one relationship)
        public Payment Payment { get; set; }
    }
}
OrderItem Models

Create a file named OrderItem.cs in the Models folder, and then copy and paste the following code. This Model represents an individual item within an order. It stores the product being purchased, the quantity ordered, and the unit price and includes a computed property for the total (quantity * unit price). The model also contains foreign key properties linking to the Order and the Product.

using System.ComponentModel.DataAnnotations.Schema;
namespace OrderInvoiceSystem.Models
{
    public class OrderItem
    {
        public int Id { get; set; }

        // Foreign key to the Order.
        public int OrderId { get; set; }
        public Order Order { get; set; }

        // Foreign key to the Product.
        public int ProductId { get; set; }
        public Product Product { get; set; }
        public int Quantity { get; set; }

        [Column(TypeName = "decimal(18,2)")]
        public decimal UnitPrice { get; set; }

        //Computed Property
        [NotMapped]
        public decimal Total => Quantity * UnitPrice;
    }
}
Payment Model

Create a file named Payment.cs in the Models folder, and then copy and paste the following code. This Model tracks payment details for an order. It stores the amount paid, the payment method (e.g., CreditCard, PayPal, COD), payment status (Pending, Success, or Failed), and the date of payment. It includes a foreign key to the associated order.

using System.ComponentModel.DataAnnotations.Schema;
namespace OrderInvoiceSystem.Models
{
    public class Payment
    {
        public int Id { get; set; }
        public int OrderId { get; set; }
        public Order Order { get; set; }

        [Column(TypeName = "decimal(18,2)")]
        public decimal AmountPaid { get; set; }

        // e.g., "CreditCard", "PayPal", "COD", etc.
        public string PaymentMethod { get; set; }

        // Pending, Success or Failed
        public string PaymentStatus { get; set; }
        public DateTime PaymentDate { get; set; }
    }
}
DbContext with Seed Data

First, create a folder named Data in the project root directory. Then, inside the Data folder, create a class file named ApplicationDbContext.cs and copy and paste the following code. The ApplicationDbContext class is responsible for interacting with the database using Entity Framework Core. It defines DbSet properties for each model so that they can be queried and saved. The OnModelCreating method seeds initial data (such as sample customers and products) to help with testing and development.

using Microsoft.EntityFrameworkCore;
using OrderInvoiceSystem.Models;

namespace OrderInvoiceSystem.Data
{
    public class ApplicationDbContext : DbContext
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            // Seed Customers (2 regular and 1 admin).
            modelBuilder.Entity<Customer>().HasData(
                new Customer { Id = 1, CustomerName = "Pranaya Rout", Email = "pranayakumar7@gmail.com", Password = "password", Phone = "1234567890", BillingAddress = "123 Main St" },
                new Customer { Id = 2, CustomerName = "Mitali Rout", Email = "pranayakumar777@gmail.com", Password = "password", Phone = "0987654321", BillingAddress = "456 Park Ave" },
                new Customer { Id = 3, CustomerName = "Admin", Email = "admin@example.com", Password = "admin", Phone = "1112223333", BillingAddress = "Admin HQ" }
            );

            // Seed Products.
            modelBuilder.Entity<Product>().HasData(
                new Product
                {
                    Id = 1,
                    ProductName = "Apple iPhone 13",
                    Category = "Smartphones",
                    Description = "The latest Apple iPhone featuring the A15 Bionic chip, 5G connectivity, and an advanced dual-camera system.",
                    UnitPrice = 799.00m
                },
                new Product
                {
                    Id = 2,
                    ProductName = "Samsung Galaxy S21",
                    Category = "Smartphones",
                    Description = "A high-end smartphone with a 6.2-inch display, Exynos 2100 processor, and a versatile triple-camera setup.",
                    UnitPrice = 699.00m
                },
                new Product
                {
                    Id = 3,
                    ProductName = "Sony WH-1000XM4 Headphones",
                    Category = "Audio",
                    Description = "Industry-leading noise-canceling headphones with superior sound quality and long battery life.",
                    UnitPrice = 349.99m
                },
                new Product
                {
                    Id = 4,
                    ProductName = "Dell XPS 13 Laptop",
                    Category = "Computers",
                    Description = "A sleek and powerful ultrabook featuring a 13.4-inch display, Intel Core i7 processor, and fast SSD storage.",
                    UnitPrice = 999.99m
                },
                new Product
                {
                    Id = 5,
                    ProductName = "Amazon Echo Dot (4th Gen)",
                    Category = "Smart Home",
                    Description = "A compact smart speaker with Alexa voice assistant for controlling smart home devices and streaming music.",
                    UnitPrice = 49.99m
                }
            );
        }

        public DbSet<Template> Templates { get; set; }
        public DbSet<Customer> Customers { get; set; }
        public DbSet<Product> Products { get; set; }
        public DbSet<Order> Orders { get; set; }
        public DbSet<OrderItem> OrderItems { get; set; }
        public DbSet<Payment> Payments { get; set; }
    }
}
Modifying AppSettings.json File:

Please modify the appsettings.json file as follows. This configuration file holds application settings, including logging levels, allowed hosts, database connection strings, and email settings. EF Core uses the connection string to connect to the SQL Server database, and the email service uses the email settings to send notifications using Google SMTP.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "DefaultConnection": "Server=LAPTOP-6P5NK25R\\SQLSERVER2022DEV;Database=ECommerceDB;Trusted_Connection=True;TrustServerCertificate=True;"
  },
  "EmailSettings": {
    "SmtpServer": "smtp.gmail.com",
    "SmtpPort": "587",
    "SenderName": "My Estore App",
    "SenderEmail": "[email protected]",
    "Password": "YOUR_GMAIL_APP_PASSWORD"
  }
}
Configure Services and Middleware in the Program.cs

Please modify the Program.cs class file as follows. The entry point of the application. It configures services for controllers with views, EF Core (with SQL Server), and cookie-based authentication. It then builds and runs the web application. This is where middleware is added to the HTTP request pipeline.

using Microsoft.EntityFrameworkCore;
using OrderInvoiceSystem.Data;
using Microsoft.AspNetCore.Authentication.Cookies;

namespace OrderInvoiceSystem
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            // Add services to the container.
            builder.Services.AddControllersWithViews();

            // EF Core
            builder.Services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

            //Cookies Based Authentication
            builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
            .AddCookie(options =>
            {
                options.LoginPath = "/Account/Login";
            });

            var app = builder.Build();

            // Configure the HTTP request pipeline.
            if (!app.Environment.IsDevelopment())
            {
                app.UseExceptionHandler("/Home/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();

            app.UseAuthentication();
            app.UseAuthorization();

            app.MapControllerRoute(
                name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}");

            app.Run();
        }
    }
}
Database Migrations

Open the Package Manager Console in Visual Studio, and then execute the following commands:

  • Add-Migration Mig1
  • Update-Database

This should be the ECommerceDB with the required Templates, Customers, Products, Orders, Order Items, and Payments tables, as shown in the image below.

Real-time Ecommerce Application using ASP.NET Core MVC

User Authentication (Customer Login / Registration)

For simplicity, we will implement a basic Authentication controller using cookie-based authentication. In production, consider using ASP.NET Core Identity. First, let us create the required view models for Customer Registration and Login, and then we will create the Authentication Controller and Views.

Creating ViewModels:

ViewModels carry data from the controller to the view and can include only the fields required for the UI. They are often used for authentication and for pages where only a subset of the data is needed. First, create a ViewModels folder in the Project root directory to create all our view models.

RegisterViewModel.cs

Create a class file named RegisterViewModel.cs within the ViewModels folder and copy and paste the following code. This class file is used during customer registration and contains fields for the customer’s full name, email, password, phone, and billing address.

using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

namespace OrderInvoiceSystem.ViewModels
{
    public class RegisterViewModel
    {
        [Required]
        [DisplayName("Full Name")]
        public string CustomerName { get; set; }

        [Required]
        [EmailAddress]
        public string Email { get; set; }

        [Required]
        [DataType(DataType.Password)]
        public string Password { get; set; }

        public string Phone { get; set; }
        [DisplayName("Billing Address")]
        public string BillingAddress { get; set; }
    }
}
LoginViewModel

Create a class file named LoginViewModel.cs and copy and paste the following code. It is used during customer login. It requires the customer’s email and password.

using System.ComponentModel.DataAnnotations;
namespace OrderInvoiceSystem.ViewModels
{
    public class LoginViewModel
    {
        [Required]
        public string Email { get; set; }

        [Required]
        [DataType(DataType.Password)]
        public string Password { get; set; }
    }
}
Account Controller

Create an MVC Empty Controller named AccountController within the Controllers folder and then copy and paste the following code. The Account Controller handles user registration, login, and logout. It uses cookie-based authentication. If the email is not already registered, a new customer record is created during registration. During login, it checks the credentials, creates a set of claims, and signs the user in.

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using OrderInvoiceSystem.Data;
using OrderInvoiceSystem.Models;
using OrderInvoiceSystem.ViewModels;
using System.Security.Claims;

namespace OrderInvoiceSystem.Controllers
{
    public class AccountController : Controller
    {
        // Reference to the application's DbContext for accessing the database.
        private readonly ApplicationDbContext _context;

        // Constructor that receives the DbContext via dependency injection.
        public AccountController(ApplicationDbContext context)
        {
            _context = context;
        }

        // GET: Account/Register
        // Renders the registration page.
        public IActionResult Register()
        {
            return View();
        }

        // POST: Account/Register
        // Handles form submission for registering a new customer.
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Register(RegisterViewModel model)
        {
            // Check if the input model is valid.
            if (ModelState.IsValid)
            {
                // Check if a customer with the provided email already exists.
                if (await _context.Customers.AnyAsync(c => c.Email == model.Email))
                {
                    ModelState.AddModelError("", "Email already registered.");
                    return View(model); // Return view with error message.
                }

                // Create a new Customer instance from the registration data.
                var customer = new Customer
                {
                    CustomerName = model.CustomerName,
                    Email = model.Email,
                    Password = model.Password, // In production, hash the password.
                    Phone = model.Phone,
                    BillingAddress = model.BillingAddress
                };

                // Add the new customer to the database context.
                _context.Customers.Add(customer);

                // Save changes asynchronously.
                await _context.SaveChangesAsync();

                // Redirect to the Login page after successful registration.
                return RedirectToAction("Login");
            }
            // If model validation fails, redisplay the registration form with validation messages.
            return View(model);
        }

        // GET: Account/Login
        // Renders the login page.
        public IActionResult Login()
        {
            return View();
        }

        // POST: Account/Login
        // Processes the login form submission.
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Login(LoginViewModel model)
        {
            // Check if the input model is valid.
            if (ModelState.IsValid)
            {
                // Attempt to find a customer with matching email and password.
                var customer = _context.Customers.AsNoTracking()
                    .FirstOrDefault(c => c.Email == model.Email && c.Password == model.Password);

                if (customer != null)
                {
                    // Create claims for the authenticated user.
                    var claims = new List<Claim>
                    {
                        new Claim(ClaimTypes.NameIdentifier, customer.Id.ToString()),
                        new Claim(ClaimTypes.Name, customer.CustomerName),
                        new Claim(ClaimTypes.Email, customer.Email)
                    };

                    // Create a ClaimsIdentity with the specified claims and authentication scheme.
                    // Cookie Based Authentication
                    var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);

                    // Sign in the user with the created identity.
                    await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme,
                        new ClaimsPrincipal(claimsIdentity));

                    // Redirect to the Order Index page upon successful login.
                    return RedirectToAction("Index", "Order");
                }
                // If no matching customer is found, add an error message.
                ModelState.AddModelError("", "Invalid login attempt.");
            }
            // If model validation fails or login unsuccessful, redisplay the login form.
            return View(model);
        }

        // GET: Account/Logout
        // Logs out the user.
        public async Task<IActionResult> Logout()
        {
            // Sign the user out of the cookie-based authentication scheme.
            await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);

            // Redirect to the Login page after logout.
            return RedirectToAction("Login");
        }
    }
}
Register (GET/POST):
  • GET: Renders the registration page.
  • POST: Validates the incoming registration data. If a customer with the same email exists, an error is added; otherwise, a new customer is created and saved to the database, then the user is redirected to the Login page.
Login (GET/POST):
  • GET: Renders the login page.
  • POST: Validates the login credentials. If valid, it creates a set of claims, generates an authentication cookie, and redirects the user to the Order page. If invalid, adds an error and redisplays the login form.
Logout (GET):
  • Signs the user out by removing the authentication cookie and redirects them to the Login page.
Account Views

Create views for Login and Register under the Views/Account folder.

Register View

Create a view named Register.cshtml within the Views/Account folder, then copy and paste the following code. This View provides a form for new users to register. It uses the RegisterViewModel for data binding.

@model OrderInvoiceSystem.ViewModels.RegisterViewModel
@{
    ViewData["Title"] = "Register";
}

<div class="container mt-5">
    <div class="row justify-content-center">
        <div class="col-md-6">
            <div class="card shadow-sm">
                <div class="card-header bg-primary text-white text-center">
                    <h3 class="mb-0">Create Your Account</h3>
                </div>
                <div class="card-body">
                    <form asp-controller="Account" asp-action="Register" method="post">
                        <div class="mb-3">
                            <label asp-for="CustomerName" class="form-label"></label>
                            <input asp-for="CustomerName" class="form-control" placeholder="Enter your full name" />
                            <span asp-validation-for="CustomerName" class="text-danger"></span>
                        </div>
                        <div class="mb-3">
                            <label asp-for="Email" class="form-label"></label>
                            <input asp-for="Email" class="form-control" placeholder="you@example.com" />
                            <span asp-validation-for="Email" class="text-danger"></span>
                        </div>
                        <div class="mb-3">
                            <label asp-for="Password" class="form-label"></label>
                            <input asp-for="Password" type="password" class="form-control" placeholder="Choose a secure password" />
                            <span asp-validation-for="Password" class="text-danger"></span>
                        </div>
                        <div class="mb-3">
                            <label asp-for="Phone" class="form-label"></label>
                            <input asp-for="Phone" class="form-control" placeholder="(555) 123-4567" />
                            <span asp-validation-for="Phone" class="text-danger"></span>
                        </div>
                        <div class="mb-3">
                            <label asp-for="BillingAddress" class="form-label"></label>
                            <input asp-for="BillingAddress" class="form-control" placeholder="Your billing address" />
                            <span asp-validation-for="BillingAddress" class="text-danger"></span>
                        </div>
                        <div class="d-grid">
                            <button type="submit" class="btn btn-success btn-lg">Register</button>
                        </div>
                    </form>
                </div>
                <div class="card-footer text-center">
                    <small>Already have an account? <a asp-controller="Account" asp-action="Login">Login here</a></small>
                </div>
            </div>
        </div>
    </div>
</div>
Login View

Create a view named Login.cshtml within the Views/Account folder and then copy and paste the following code. This View provides a form for users to log in using the LoginViewModel for data binding.

@model OrderInvoiceSystem.ViewModels.LoginViewModel
@{
    ViewData["Title"] = "Login";
}

<div class="container mt-5">
    <div class="row justify-content-center">
        <div class="col-md-5">
            <div class="card shadow-sm">
                <div class="card-header bg-primary text-white text-center">
                    <h3 class="mb-0">Customer Login</h3>
                </div>
                <div class="card-body">
                    <form asp-controller="Account" asp-action="Login" method="post">
                        <div class="mb-3">
                            <label asp-for="Email" class="form-label"></label>
                            <input asp-for="Email" class="form-control" placeholder="you@example.com" />
                            <span asp-validation-for="Email" class="text-danger"></span>
                        </div>
                        <div class="mb-3">
                            <label asp-for="Password" class="form-label"></label>
                            <input asp-for="Password" type="password" class="form-control" placeholder="Enter your password" />
                            <span asp-validation-for="Password" class="text-danger"></span>
                        </div>
                        <div class="d-grid">
                            <button type="submit" class="btn btn-primary btn-lg">Login</button>
                        </div>
                    </form>
                </div>
                <div class="card-footer text-center">
                    <small>
                        Don't have an account? <a asp-controller="Account" asp-action="Register">Register here</a>
                    </small>
                </div>
            </div>
        </div>
    </div>
</div>

Order and Payment Flow

A logged-in customer can create an order. For simplicity, let’s assume there’s a page where the customer can select products and quantities. (You can expand this as needed.)

OrderCreateViewModel

These view models are used when a customer creates a new order. They represent the data submitted from the order creation page.

  • OrderCreateViewModel contains a list of order items.
  • OrderItemCreateViewModel represents a single line item in the order (i.e., the product being ordered and the quantity).

Create a class file named OrderCreateViewModel.cs with the ViewModels folder, and then copy and paste the following code. This view model is used for order creation.

using System.ComponentModel.DataAnnotations;
namespace OrderInvoiceSystem.ViewModels
{
    public class OrderCreateViewModel
    {
        // In a real app, you would have a shopping cart.
        [Required]
        public List<OrderItemCreateViewModel> OrderItems { get; set; }
    }

    public class OrderItemCreateViewModel
    {
        [Required]
        public int ProductId { get; set; }
        [Required]
        public int Quantity { get; set; }
    }
}

When the customer submits their product selections, the controller receives an instance of OrderCreateViewModel, which contains a list of OrderItemCreateViewModel objects. The controller then uses this data to create an Order and its related OrderItem records in the database.

PaymentViewModel

Create a class file named PaymentViewModel.cs within the ViewModels folder, and then copy and paste the following code. This view model is used for payment creation.

namespace OrderInvoiceSystem.ViewModels
{
    public class PaymentViewModel
    {
        public int OrderId {  get; set; }
        public string? OrderNumber { get; set; }
        public decimal TotalAmount {  get; set; }
        public string PaymentMethod { get; set; }
    }
}
OrderController

Create an MVC Empty Controller named OrderController within the Controllers folder and then copy and paste the following code. The OrderController manages the entire order lifecycle for a logged-in customer. It handles order listing, creation, payment processing, and invoice generation. It also demonstrates how to send an invoice via email using SMTP and generate a PDF invoice using IronPdf.

using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using OrderInvoiceSystem.Data;
using OrderInvoiceSystem.Models;
using OrderInvoiceSystem.ViewModels;
using System.Net;
using System.Net.Mail;
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;

namespace OrderInvoiceSystem.Controllers
{
    // Only authenticated users can access these actions.
    [Authorize]
    public class OrderController : Controller
    {
        // The database context used to access the data.
        private readonly ApplicationDbContext _context;
        
        // The configuration object to read settings (like email settings) from appsettings.json.
        private readonly IConfiguration _configuration;

        // Constructor that receives ApplicationDbContext and IConfiguration via dependency injection.
        public OrderController(ApplicationDbContext context, IConfiguration configuration)
        {
            _context = context;
            _configuration = configuration;
        }

        // GET: Order/Index
        // Displays a list of orders for the currently logged-in customer.
        public async Task<IActionResult> Index()
        {
            // Retrieve the logged-in user's CustomerId from the claims.
            var customerIdClaim = User.FindFirst(ClaimTypes.NameIdentifier);
            if (customerIdClaim == null)
                return RedirectToAction("Login", "Account");

            int customerId = int.Parse(customerIdClaim.Value);

            // Query orders for this customer, including the order items.
            var orders = await _context.Orders
                .AsNoTracking()  // Improves performance for read-only queries.
                .Include(o => o.OrderItems)
                .Where(o => o.CustomerId == customerId)
                .ToListAsync();

            // Pass the list of orders to the view.
            return View(orders);
        }

        // GET: Order/Create
        // Displays the order creation form.
        public IActionResult Create()
        {
            // For simplicity, we pass the list of available products using ViewBag.
            ViewBag.Products = _context.Products.AsNoTracking().ToList();
            return View();
        }

        // POST: Order/Create
        // Processes the submitted order creation form.
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Create(OrderCreateViewModel model)
        {
            // If the model state is invalid, return the view with validation errors.
            if (!ModelState.IsValid)
                return View(model);

            // Retrieve the logged-in customer's ID from the claims.
            var customerIdClaim = User.FindFirst(ClaimTypes.NameIdentifier);
            if (customerIdClaim == null)
                return RedirectToAction("Login", "Account");

            int customerId = int.Parse(customerIdClaim.Value);

            // Generate a unique order number using current ticks.
            var orderNumber = $"ORD-{DateTime.UtcNow.Ticks}";

            // Create a new Order entity with initial values.
            var order = new Order
            {
                CustomerId = customerId,
                OrderNumber = orderNumber,
                OrderDate = DateTime.UtcNow,
                OrderStatus = "Pending", // New orders start with "Pending" status.
                CreatedDate = DateTime.UtcNow,
                OrderItems = new List<OrderItem>()
            };

            decimal totalAmount = 0;

            // Iterate through each submitted order item.
            foreach (var item in model.OrderItems)
            {
                // Only include items where the quantity is greater than 0.
                if (item.Quantity <= 0)
                    continue;

                // Retrieve the product from the database using its ID.
                var product = await _context.Products.FindAsync(item.ProductId);
                if (product == null)
                    continue;

                // Create a new OrderItem entity.
                var orderItem = new OrderItem
                {
                    ProductId = product.Id,
                    Quantity = item.Quantity,
                    UnitPrice = product.UnitPrice
                };

                // Add the computed item total to the order's total amount.
                totalAmount += orderItem.Total;

                // Add the order item to the order.
                order.OrderItems.Add(orderItem);
            }
            order.TotalAmount = totalAmount;

            // Add the new order to the context and save changes.
            _context.Orders.Add(order);
            await _context.SaveChangesAsync();

            // Redirect the customer to the Payment page for this order.
            return RedirectToAction("Payment", new { orderId = order.Id });
        }

        // GET: Order/Payment
        // Displays the payment page with order summary details.
        public async Task<IActionResult> Payment(int orderId)
        {
            // Retrieve the order including customer information.
            var paymentViewModel = await _context.Orders.AsNoTracking()
                .Select( order => new PaymentViewModel()
                {
                    OrderId = order.Id,
                    OrderNumber = order.OrderNumber,
                    TotalAmount = order.TotalAmount
                })
                .FirstOrDefaultAsync(o => o.OrderId == orderId);

            if (paymentViewModel == null)
                return NotFound();

            // Store the order in ViewBag (for this simplified example) to display summary data.
            //ViewBag.Order = order;
            return View(paymentViewModel);
        }

        // POST: Order/Payment
        // Processes the payment for the order.
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Payment(PaymentViewModel paymentViewModel)
        {
            // Retrieve the order (including customer) from the database.
            var order = await _context.Orders
                .Include(o => o.Customer)
                .FirstOrDefaultAsync(o => o.Id == paymentViewModel.OrderId);

            if (order == null)
                return NotFound();

            // Simulate payment processing based on selected payment method.
            // For example: if PayPal is chosen, simulate failure; if COD, set order to pending; otherwise, complete the order.
            string paymentStatus = "";
            if (paymentViewModel.PaymentMethod.ToUpper() == "PAYPAL")
            {
                order.OrderStatus = "Cancelled";
                paymentStatus = "Failed";
            }
            else if (paymentViewModel.PaymentMethod.ToUpper() == "COD")
            {
                order.OrderStatus = "Pending";
                paymentStatus = "Pending";
            }
            else
            {
                order.OrderStatus = "Completed";
                paymentStatus = "Success";
            }

            // Create a new Payment record.
            var payment = new Payment
            {
                OrderId = order.Id,
                AmountPaid = order.TotalAmount,
                PaymentMethod = paymentViewModel.PaymentMethod,
                PaymentStatus = paymentStatus,
                PaymentDate = DateTime.UtcNow
            };

            // Add the payment to the context.
            _context.Payments.Add(payment);

            // Save changes (update order status and add payment record).
            await _context.SaveChangesAsync();

            // Generate the invoice PDF and send it via email.
            byte[] pdfBytes = await GenerateInvoicePdfAsync(order.Id);
            await SendInvoiceEmail(order.Customer.Email, order.Customer.CustomerName,
                                   BuildEmailSubject(order),
                                   BuildEmailBody(order),
                                   pdfBytes);

            // Redirect to the Order Placed page, which displays the order details.
            return RedirectToAction("OrderPlaced", new { orderId = order.Id });
        }

        // GET: Order/OrderPlaced/{orderId}
        // Displays a confirmation page with the order details after the order is placed.
        public async Task<IActionResult> OrderPlaced(int orderId)
        {
            // Retrieve the order along with its order items and associated products, plus customer details.
            var order = await _context.Orders.AsNoTracking()
                .Include(o => o.OrderItems)
                    .ThenInclude(oi => oi.Product)
                .FirstOrDefaultAsync(o => o.Id == orderId);

            if (order == null)
                return NotFound();

            // Pass the order to the view.
            return View(order);
        }

        // GET: Order/GenerateInvoice/{orderId}?sendEmail=false
        // Generates a PDF invoice for the given order.
        // If sendEmail is true, the invoice is emailed to the customer; otherwise, it is returned as a file.
        [AllowAnonymous]
        public async Task<IActionResult> GenerateInvoice(int orderId, bool sendEmail = false)
        {
            // Generate the invoice PDF.
            byte[] pdfBytes = await GenerateInvoicePdfAsync(orderId);

            // If sendEmail flag is true, look up the order and email the invoice.
            if (sendEmail)
            {
                var order = await _context.Orders.AsNoTracking()
                    .Include(o => o.Customer)
                    .FirstOrDefaultAsync(o => o.Id == orderId);

                if (order != null)
                {
                    await SendInvoiceEmail(order.Customer.Email, order.Customer.CustomerName,
                                           BuildEmailSubject(order),
                                           BuildEmailBody(order),
                                           pdfBytes);
                }
            }

            // Return the PDF file for download.
            return File(pdfBytes, "application/pdf", $"OrderInvoice_{orderId}.pdf");
        }

        // Helper method to generate invoice PDF using IronPdf.
        // Loads the order details, selects an appropriate invoice template based on the order status,
        // replaces placeholders in the HTML with real order values, and returns the PDF as a byte array.
        private async Task<byte[]> GenerateInvoicePdfAsync(int orderId)
        {
            // Retrieve the order along with order items, associated products, and customer info.
            var order = await _context.Orders.AsNoTracking()
                .Include(o => o.OrderItems)
                    .ThenInclude(oi => oi.Product)
                .Include(o => o.Customer)
                .Include(o => o.Payment)
                .FirstOrDefaultAsync(o => o.Id == orderId);

            if (order == null)
                throw new Exception("Order not found.");

            // Retrieve the associated payment record, if available.
            // var payment = await _context.Payments.FirstOrDefaultAsync(p => p.OrderId == order.Id);

            // Determine which invoice template to use based on the order status.
            TemplateType templateType;
            if (order.OrderStatus.Equals("Completed", StringComparison.OrdinalIgnoreCase))
            {
                templateType = TemplateType.OrderCompletion;
            }   
            else if (order.OrderStatus.Equals("Cancelled", StringComparison.OrdinalIgnoreCase))
            {
                templateType = TemplateType.OrderCancelled;
            }   
            else
            {
                // For pending orders, use the pending template.
                templateType = TemplateType.OrderPending;
            }
              
            // Retrieve the invoice template from the database.
            var template = await _context.Templates.FirstOrDefaultAsync(t => t.TemplateType == templateType);
            if (template == null)
                throw new Exception("Invoice template not found.");

            // Build the HTML string for the product list by iterating through the order items.
            string productListHtml = "";
            foreach (var item in order.OrderItems)
            {
                productListHtml += $"<tr>" +
                                   $"<td style='padding:12px; border:1px solid #ddd;'>{item.Product.ProductName}</td>" +
                                   $"<td style='padding:12px; border:1px solid #ddd;'>{item.Quantity}</td>" +
                                   $"<td style='padding:12px; border:1px solid #ddd;'>{item.UnitPrice:C}</td>" +
                                   $"<td style='padding:12px; border:1px solid #ddd;'>{item.Total:C}</td>" +
                                   $"</tr>";
            }
            // Append closing tags for the table body if needed (assuming the template includes the table header).
            productListHtml += "</tbody></table>";

            // Replace placeholders in the HTML template with actual order values.
            string htmlContent = template.HtmlContent;
            htmlContent = ReplacePlaceholder(htmlContent, "CustomerName", order.Customer.CustomerName);
            htmlContent = ReplacePlaceholder(htmlContent, "OrderDate", order.OrderDate.ToString("MMMM dd, yyyy"));
            htmlContent = ReplacePlaceholder(htmlContent, "OrderNumber", order.OrderNumber);
            htmlContent = ReplacePlaceholder(htmlContent, "OrderStatus", order.OrderStatus);
            htmlContent = ReplacePlaceholder(htmlContent, "ProductList", productListHtml);
            htmlContent = ReplacePlaceholder(htmlContent, "TotalAmount", order.TotalAmount.ToString("C"));

            // Replace payment placeholders if a payment record exists.
            if (order.Payment != null)
            {
                htmlContent = ReplacePlaceholder(htmlContent, "PaymentStatus", order.Payment.PaymentStatus);
                htmlContent = ReplacePlaceholder(htmlContent, "PaymentMethod", order.Payment.PaymentMethod);
                htmlContent = ReplacePlaceholder(htmlContent, "PaymentDate", order.Payment.PaymentDate.ToString("MMMM dd, yyyy"));
            }

            // Create an instance of ChromePdfRenderer from IronPdf.
            var renderer = new ChromePdfRenderer();

            // Optional: If you have a license key, set it before rendering
            // IronPdf.License.LicenseKey = "Your IRON PDF License";

            // Render the HTML string to a PDF document.
            var pdfDocument = renderer.RenderHtmlAsPdf(htmlContent);

            // Return the PDF document as a byte array.
            return pdfDocument.BinaryData;
        }

        // Helper method for placeholder replacement.
        // Replaces occurrences of a placeholder (e.g., {CustomerName}) in the HTML with the provided value.
        private string ReplacePlaceholder(string html, string placeholder, string value)
        {
            return html.Replace("{" + placeholder + "}", value);
        }

        // Helper method to build a dynamic email subject based on the order status.
        private string BuildEmailSubject(Order order)
        {
            if (order.OrderStatus.Equals("Completed", StringComparison.OrdinalIgnoreCase))
                return $"Your Order Invoice - Order #{order.OrderNumber} Completed";
            else if (order.OrderStatus.Equals("Cancelled", StringComparison.OrdinalIgnoreCase))
                return $"Your Order Invoice - Order #{order.OrderNumber} Cancelled";
            else
                return $"Your Order Invoice - Order #{order.OrderNumber} Pending";
        }

        // Helper method to build a dynamic email body based on the order status.
        private string BuildEmailBody(Order order)
        {
            if (order.OrderStatus.Equals("Completed", StringComparison.OrdinalIgnoreCase))
            {
                return $"Dear {order.Customer.CustomerName},<br/><br/>" +
                       $"Thank you for your order. Your order #{order.OrderNumber} has been completed successfully. " +
                       $"Please find attached your invoice.<br/><br/>Best regards,<br/>My Estore App";
            }
            else if (order.OrderStatus.Equals("Cancelled", StringComparison.OrdinalIgnoreCase))
            {
                return $"Dear {order.Customer.CustomerName},<br/><br/>" +
                       $"We regret to inform you that your order #{order.OrderNumber} has been cancelled. " +
                       $"Please contact our support for further details.<br/><br/>Best regards,<br/>My Estore App";
            }
            else
            {
                return $"Dear {order.Customer.CustomerName},<br/><br/>" +
                       $"Please find attached your invoice for order #{order.OrderNumber}.<br/><br/>Best regards,<br/>My Estore App";
            }
        }

        // Helper method to send the invoice PDF via email using SMTP.
        // Retrieves SMTP settings from configuration, creates an email message with the PDF attached, and sends it.
        private async Task SendInvoiceEmail(string toEmail, string customerName, string subject, string htmlBody, byte[] pdfAttachment, bool isBodyHtml = true)
        {
            // Retrieve email settings from appsettings.json.
            string smtpServer = _configuration.GetValue<string>("EmailSettings:SmtpServer") ?? "";
            int smtpPort = int.Parse(_configuration.GetValue<string>("EmailSettings:SmtpPort") ?? "587");
            string senderName = _configuration.GetValue<string>("EmailSettings:SenderName") ?? "My Estore App";
            string senderEmail = _configuration.GetValue<string>("EmailSettings:SenderEmail") ?? "";
            string password = _configuration.GetValue<string>("EmailSettings:Password") ?? "";

            // Create a new MailMessage.
            using (var message = new MailMessage())
            {
                message.From = new MailAddress(senderEmail, senderName);
                message.To.Add(new MailAddress(toEmail));
                message.Subject = subject;
                message.Body = htmlBody;
                message.IsBodyHtml = isBodyHtml;

                // Attach the PDF invoice.
                message.Attachments.Add(new Attachment(new MemoryStream(pdfAttachment), "OrderInvoice.pdf", "application/pdf"));

                // Create an SMTP client using the provided settings.
                using (var client = new SmtpClient(smtpServer, smtpPort))
                {
                    client.Credentials = new NetworkCredential(senderEmail, password);
                    client.EnableSsl = true;
                    // Send the email asynchronously.
                    await client.SendMailAsync(message);
                }
            }
        }
    }
}
Index Action:
  • Retrieves the logged-in customer’s orders from the database and returns them to the view.
Create Actions (GET/POST):
  • GET: Loads available products for selection (via ViewBag).
  • POST: Validates order input, creates an order and its items, calculates the total, saves the order, and then redirects to the Payment page.
Payment Actions (GET/POST):
  • GET: Retrieves the order details and displays the payment form.
  • POST: This method simulates payment processing based on the selected payment method, creates a payment record, saves changes, generates the invoice PDF, sends it via email, and redirects it to a confirmation page.
OrderPlaced Action:
  • Retrieves and displays detailed information for the completed order.
GenerateInvoice Action:
  • Generates a PDF invoice (using IronPdf), optionally sends it via email, and returns the PDF as a downloadable file.
Helper Methods:
  • Provide reusable functionality for generating PDFs, replacing placeholders in templates, building dynamic email content, and sending emails with invoice attachments.
Order Views
Index.cshtml

Create a view named Index.cshtml within the Views/Order folder and then copy and paste the following code. This View displays a list of orders for the logged-in customer. If there are no orders, it shows an alert message. Otherwise, it renders a responsive table with each order’s number, date, status (with Bootstrap badges for different statuses), total amount, and an action button to download the invoice.

@model IEnumerable<OrderInvoiceSystem.Models.Order>

@{
    ViewData["Title"] = "My Orders";
}

<div class="container" style="max-width: 900px;">
    <div class="card my-5 shadow">
        <div class="card-header bg-info text-white">
            <h3 class="card-title mb-0">@ViewData["Title"]</h3>
        </div>
        <div class="card-body">

            @if (!Model.Any())
            {
                <div class="alert alert-warning text-center" role="alert">
                    You currently have no orders.
                </div>
            }
            else
            {
                <table class="table table-hover align-middle">
                    <thead class="table-light">
                        <tr>
                            <th>Order Number</th>
                            <th>Order Date</th>
                            <th>Status</th>
                            <th>Total</th>
                            <th class="text-center">Actions</th>
                        </tr>
                    </thead>
                    <tbody>
                        @foreach (var order in Model)
                        {
                            <tr>
                                <td>@order.OrderNumber</td>
                                <td>@order.OrderDate.ToString("g")</td>
                                <td>
                                    @* Conditionally render a Bootstrap badge based on the status *@
                                    @if (order.OrderStatus == "Completed")
                                    {
                                        <span class="badge bg-success">Completed</span>
                                    }
                                    else if (order.OrderStatus == "Cancelled")
                                    {
                                        <span class="badge bg-danger">Cancelled</span>
                                    }
                                    else
                                    {
                                        <span class="badge bg-warning text-dark">Pending</span>
                                    }
                                </td>
                                <td>@order.TotalAmount.ToString("C")</td>
                                <td class="text-center">
                                    <a asp-action="GenerateInvoice"
                                       asp-route-orderId="@order.Id"
                                       class="btn btn-primary btn-sm">
                                        Download Invoice
                                    </a>
                                </td>
                            </tr>
                        }
                    </tbody>
                </table>
            }
        </div>
    </div>
</div>
Create.cshtml

The Create View renders a page where the customer can create a new order.

  • Displays a list of available products with unit prices.
  • The view provides plus and minus buttons for each product to adjust the quantity and a Remove button.
  • It also shows an Item Total for each row and dynamically calculates a “Grand Total” using JavaScript.
  • When the form is submitted, the selected products and their quantities are posted via the OrderCreateViewModel.

So, create a view named Create.cshtml within the Views/Order folder and copy and paste the following code. This view is used to create a new order.

@model OrderInvoiceSystem.ViewModels.OrderCreateViewModel
@using OrderInvoiceSystem.Models

@{
    ViewData["Title"] = "Place Order";
    var products = ViewBag.Products as List<Product> ?? new List<Product>();
}

<div class="container" style="max-width: 900px;">
    <div class="card my-5 shadow">
        <div class="card-header bg-primary text-white">
            <h3 class="card-title mb-0">@ViewData["Title"]</h3>
        </div>
        <div class="card-body">
            @if (!products.Any())
            {
                <div class="alert alert-warning text-center" role="alert">
                    No products available to order.
                </div>
            }
            else
            {
                <!-- Informational message guiding user actions -->
                <div class="alert alert-info" role="alert">
                    <strong>Review your order before submitting.</strong><br />
                    Use the <strong>+</strong> or <strong>-</strong> buttons to adjust quantities,
                    and click <strong>Remove</strong> if you no longer want an item.
                </div>

                <!-- Begin Form -->
                <form asp-action="Create" method="post">
                    <table class="table table-bordered table-hover align-middle" id="orderTable">
                        <thead class="table-light">
                            <tr>
                                <th>Product</th>
                                <th>Unit Price</th>
                                <th style="width: 200px;">Quantity</th>
                                <th>Item Total</th>
                                <th class="text-center">Actions</th>
                            </tr>
                        </thead>
                        <tbody>
                            @for (int i = 0; i < products.Count; i++)
                            {
                                var product = products[i];
                                <tr data-index="@i" data-unit-price="@product.UnitPrice">
                                    <!-- Product Name -->
                                    <td class="fw-bold">
                                        @product.ProductName
                                        <input type="hidden" name="OrderItems[@i].ProductId" value="@product.Id" />
                                    </td>

                                    <!-- Unit Price -->
                                    <td>
                                        @product.UnitPrice.ToString("C")
                                    </td>

                                    <!-- Quantity with +/- buttons -->
                                    <td class="text-center">
                                        <div class="d-flex justify-content-start align-items-center">
                                            <button type="button" class="btn btn-sm btn-secondary me-2 qty-minus">-</button>
                                            <span class="quantity-val fw-bold">0</span>
                                            <button type="button" class="btn btn-sm btn-secondary ms-2 qty-plus">+</button>
                                        </div>
                                        <!-- Hidden input to store final quantity for form submission -->
                                        <input type="hidden" name="OrderItems[@i].Quantity" value="0" class="qty-input" />
                                    </td>

                                    <!-- Item Total -->
                                    <td class="fw-bold">
                                        <span class="item-total">0.00</span>
                                    </td>

                                    <!-- Remove Button -->
                                    <td class="text-center">
                                        <button type="button" class="btn btn-danger btn-sm remove-item">
                                            Remove
                                        </button>
                                    </td>
                                </tr>
                            }
                        </tbody>
                    </table>

                    <!-- Grand Total Display -->
                    <div class="d-flex justify-content-end align-items-center mb-3">
                        <h4 class="me-2 mb-0">Grand Total: </h4>
                        <h4 class="mb-0 text-success">₹<span id="grandTotal">0.00</span></h4>
                    </div>

                    <div class="d-grid">
                        <button type="submit" class="btn btn-success">Place Order</button>
                    </div>
                </form>
                <!-- End Form -->
            }
        </div>
    </div>
</div>

@section Scripts {
    <script>
        // On DOM load, attach events
        document.addEventListener("DOMContentLoaded", function () {
            const table = document.getElementById("orderTable");
            if (!table) return;

            const grandTotalEl = document.getElementById("grandTotal");

            // Function to recalc item total
            function recalcItem(row) {
                const unitPrice = parseFloat(row.dataset.unitPrice);
                const qtyInput = row.querySelector(".qty-input");
                const qty = parseInt(qtyInput.value) || 0;

                const itemTotal = unitPrice * qty;
                // Update the item total cell
                row.querySelector(".item-total").textContent = itemTotal.toFixed(2);
            }

            // Function to recalc the grand total for all items
            function recalcGrandTotal() {
                let grandTotal = 0;
                const rows = table.querySelectorAll("tbody tr");
                rows.forEach(row => {
                    const itemTotalEl = row.querySelector(".item-total");
                    if (itemTotalEl) {
                        grandTotal += parseFloat(itemTotalEl.textContent) || 0;
                    }
                });
                grandTotalEl.textContent = grandTotal.toFixed(2);
            }

            // Recalc both item total and grand total
            function recalcAll(row) {
                recalcItem(row);
                recalcGrandTotal();
            }

            // Event delegation for plus/minus/remove
            table.addEventListener("click", function (e) {
                const target = e.target;

                // If user clicked minus
                if (target.classList.contains("qty-minus")) {
                    const row = target.closest("tr");
                    const qtySpan = row.querySelector(".quantity-val");
                    const qtyInput = row.querySelector(".qty-input");
                    let currentQty = parseInt(qtyInput.value) || 0;

                    if (currentQty > 0) currentQty--;
                    qtySpan.textContent = currentQty;
                    qtyInput.value = currentQty;

                    recalcAll(row);
                }

                // If user clicked plus
                if (target.classList.contains("qty-plus")) {
                    const row = target.closest("tr");
                    const qtySpan = row.querySelector(".quantity-val");
                    const qtyInput = row.querySelector(".qty-input");
                    let currentQty = parseInt(qtyInput.value) || 0;

                    currentQty++;
                    qtySpan.textContent = currentQty;
                    qtyInput.value = currentQty;

                    recalcAll(row);
                }

                // If user clicked remove
                if (target.classList.contains("remove-item")) {
                    const row = target.closest("tr");
                    row.remove(); // remove row from table
                    recalcGrandTotal(); // update total after removing
                }
            });
        });
    </script>
}
Payment.cshtml

The Payment view gives the customer a payment form for the selected order.

  • Displays key order details (like order number and total amount).
  • A dropdown to select a payment method (Credit Card, PayPal, or COD) is included.
  • When the form is submitted, the selected payment method is posted, and the Payment action in the controller processes the payment accordingly.

Create a view named Payment.cshtml within the Views/Order folder and then copy and paste the following code. This view is used to process the Payment of an order.

@model OrderInvoiceSystem.ViewModels.PaymentViewModel

@{
    ViewData["Title"] = "Make Payment";
}

<div class="container mt-5">
    <div class="row justify-content-center">
        <div class="col-md-6">
            <div class="card shadow-sm">
                <div class="card-header bg-info text-white text-center">
                    <h3 class="mb-0">Payment</h3>
                </div>
                <div class="card-body">
                    <div class="mb-3">
                        <label class="form-label"><strong>Order Number:</strong></label>
                        <p>@Model?.OrderNumber</p>
                    </div>
                    <div class="mb-3">
                        <label class="form-label"><strong>Total Amount:</strong></label>
                        <p>@Model?.TotalAmount.ToString("C")</p>
                    </div>
                    <form asp-action="Payment" method="post">
                        <input type="hidden" name="OrderId" value="@Model?.OrderId" />
                        <div class="mb-3">
                            <label class="form-label">Payment Method</label>
                            <select name="PaymentMethod" class="form-select">
                                <option value="">-- Select Payment Method --</option>
                                <option value="CreditCard">Credit Card</option>
                                <option value="PayPal">PayPal</option>
                                <option value="COD">COD</option>
                            </select>
                        </div>
                        <div class="d-grid">
                            <button type="submit" class="btn btn-primary btn-lg">Pay Now</button>
                        </div>
                    </form>
                </div>
                <div class="card-footer text-center">
                    <small class="text-muted">Please review your order details before making the payment.</small>
                </div>
            </div>
        </div>
    </div>
</div>
OrderPlaced.cshtml

The OrderPlaced view Shows a detailed confirmation page after the order has been processed (order placed).

  • Displays a header with a message that changes based on the order’s status (Completed, Cancelled, or Pending).
  • Shows order details summary (number, date, status, and total amount).
  • Lists each order item with product name, quantity, unit price, and item total.
  • It provides action buttons for downloading or viewing the invoice and returning to the order list.

Create a view named OrderPlaced.cshtml within the Views/Order folder, then copy and paste the following code.

@model OrderInvoiceSystem.Models.Order
@{
    ViewData["Title"] = "Order Placed";

    // Determine header message based on OrderStatus
    string headerMessage = "";
    string headerSubMessage = "";
    string headerBg = "";
    string totalTextClass = "";

    if (Model.OrderStatus.Equals("Completed", StringComparison.OrdinalIgnoreCase))
    {
        headerMessage = "Order Placed Successfully!";
        headerSubMessage = "Thank you for your purchase.";
        headerBg = "linear-gradient(90deg, #28a745, #218838)";
        totalTextClass = "text-success";
    }
    else if (Model.OrderStatus.Equals("Cancelled", StringComparison.OrdinalIgnoreCase))
    {
        headerMessage = "Order Cancelled";
        headerSubMessage = "We're sorry, your order has been cancelled.";
        headerBg = "linear-gradient(90deg, #dc3545, #c82333)";
        totalTextClass = "text-danger";
    }
    else // Pending or any other status
    {
        headerMessage = "Order Pending Payment";
        headerSubMessage = "Your order is awaiting payment confirmation.";
        headerBg = "linear-gradient(90deg, #ffc107, #e0a800)";
        totalTextClass = "text-warning";
    }
}

<div class="container mt-5">
    <div class="row justify-content-center">
        <div class="col-lg-8">
            <div class="card shadow-lg border-0">
                <div class="card-header text-center py-4" style="background: @headerBg; color:#fff;">
                    <h2 class="mb-0" style="font-size: 2rem; font-weight:700;">@headerMessage</h2>
                    <p class="lead mb-0" style="font-size:1.2rem;">@headerSubMessage</p>
                </div>
                <div class="card-body p-4">
                    <!-- Order Summary -->
                    <div class="row mb-4">
                        <div class="col-md-4 text-center">
                            <h6 class="text-uppercase text-muted">Order Number</h6>
                            <p class="fs-5 mb-0">@Model.OrderNumber</p>
                        </div>
                        <div class="col-md-4 text-center">
                            <h6 class="text-uppercase text-muted">Order Date</h6>
                            <p class="fs-5 mb-0">@Model.OrderDate.ToString("MMMM dd, yyyy")</p>
                        </div>
                        <div class="col-md-4 text-center">
                            <h6 class="text-uppercase text-muted">Status</h6>
                            <p class="fs-5 mb-0">@Model.OrderStatus</p>
                        </div>
                    </div>
                    <div class="mb-4 text-center">
                        <h6 class="text-uppercase text-muted">Total Amount</h6>
                        <p class="display-6 @totalTextClass mb-0">@Model.TotalAmount.ToString("C")</p>
                    </div>
                    <hr />
                    <!-- Order Items -->
                    <div class="mb-4">
                        <h5 class="mb-3">Order Items</h5>
                        <div class="table-responsive">
                            <table class="table table-striped">
                                <thead class="table-light">
                                    <tr>
                                        <th>Product</th>
                                        <th class="text-center">Quantity</th>
                                        <th class="text-end">Unit Price</th>
                                        <th class="text-end">Total</th>
                                    </tr>
                                </thead>
                                <tbody>
                                    @foreach (var item in Model.OrderItems)
                                    {
                                        <tr>
                                            <td>@item.Product.ProductName</td>
                                            <td class="text-center">@item.Quantity</td>
                                            <td class="text-end">@item.UnitPrice.ToString("C")</td>
                                            <td class="text-end">@item.Total.ToString("C")</td>
                                        </tr>
                                    }
                                </tbody>
                            </table>
                        </div>
                    </div>
                </div>
                <div class="card-footer bg-light py-3 text-center">
                    <a class="btn btn-primary me-2" href="@Url.Action("GenerateInvoice", "Order", new { orderId = Model.Id, sendEmail = false })">
                        Download Invoice
                    </a>
                    <a class="btn btn-secondary" href="@Url.Action("Index", "Order")">
                        Back to Orders
                    </a>
                </div>
            </div>
        </div>
    </div>
</div>

Admin Management of Templates

We will create a CRUD controller to manage the OrderInvoiceTemplate entity. Admins can manage invoice templates via CKEditor. Admin users log in via a separate admin login or a role check. You can restrict these actions with role-based authorization; for simplicity, we assume these pages are accessible only by admin users.

InvoiceTemplateViewModel.cs

Create a class file named InvoiceTemplateViewModel.cs with the ViewModels folder, and then copy and paste the following code. This view model represents the data the Create and Edit views require for invoice templates. It contains only the fields that the admin needs to input or update: the template’s name, the HTML content (which includes placeholders), and the template type (an enum indicating if the template is for a completed order, canceled order, or pending order).

using System.ComponentModel.DataAnnotations;
using OrderInvoiceSystem.Models;

namespace OrderInvoiceSystem.ViewModels
{
    public class InvoiceTemplateViewModel
    {
        public int Id { get; set; }

        [Required(ErrorMessage = "Template name is required.")]
        [MaxLength(200)]
        public string TemplateName { get; set; }

        [Required(ErrorMessage = "HTML content is required.")]
        public string HtmlContent { get; set; }

        [Required(ErrorMessage = "Template type is required.")]
        public TemplateType TemplateType { get; set; }
    }
}

How It’s Used:

  • Create.cshtml and Edit.cshtml: These views use this view model to render a form where the admin can enter or update a template.
  • Model Binding: When the form is submitted, the model binder creates an instance of InvoiceTemplateViewModel with the entered values. The controller then maps these values to the corresponding domain model (OrderInvoiceTemplate) to save to the database.
TemplateController

Create an MVC Empty Controller named TemplateController within the Controllers folder and then copy and paste the following code. This controller provides the CRUD operations (Create, Read, Update, Delete) for managing invoice templates. It is designed to be accessible only by admin users (in a real application, you might enforce this using role-based authorization).

using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using OrderInvoiceSystem.Data;
using OrderInvoiceSystem.ViewModels;
using OrderInvoiceSystem.Models;
using Microsoft.AspNetCore.Authorization;

namespace OrderInvoiceSystem.Controllers
{
    [Authorize]
    public class TemplateController : Controller
    {
        private readonly ApplicationDbContext _context;

        public TemplateController(ApplicationDbContext context)
        {
            _context = context;
        }

        // GET: Template/Index
        // Retrieves and displays a list of all invoice templates.
        public async Task<IActionResult> Index()
        {
            // Fetch all templates without tracking for performance improvement on read-only queries.
            var templates = await _context.Templates.AsNoTracking().ToListAsync();
            return View(templates);
        }

        // GET: Template/Create
        // Displays a form to create a new invoice template.
        public IActionResult Create()
        {
            // Initialize a new InvoiceTemplateViewModel and pass it to the view.
            return View(new InvoiceTemplateViewModel());
        }

        // POST: Template/Create
        // Processes the form submission for creating a new invoice template.
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Create(InvoiceTemplateViewModel model)
        {
            // Check if the form data is valid.
            if (ModelState.IsValid)
            {
                // Map the view model to the domain model.
                var template = new Template
                {
                    TemplateName = model.TemplateName,
                    HtmlContent = model.HtmlContent,
                    TemplateType = model.TemplateType,
                    CreatedDate = DateTime.Now,
                    // Set the creator; default to "Admin" if the user identity is not available.
                    CreatedBy = User.Identity?.Name ?? "Admin"
                };

                // Add the new template to the database context.
                _context.Templates.Add(template);

                // Save changes asynchronously.
                await _context.SaveChangesAsync();

                // Redirect to the Index action to show the list of templates.
                return RedirectToAction(nameof(Index));
            }
            // If validation fails, return the view with the current model to display validation errors.
            return View(model);
        }

        // GET: Template/Edit/{id}
        // Retrieves an existing invoice template for editing.
        public async Task<IActionResult> Edit(int id)
        {
            // Retrieve the template by id without tracking changes (read-only).
            var template = await _context.Templates
                .AsNoTracking().FirstOrDefaultAsync(temp => temp.Id == id);

            // If the template does not exist, return a 404 Not Found.
            if (template == null)
            {
                return NotFound();
            }

            // Map the domain model to the view model.
            var viewModel = new InvoiceTemplateViewModel
            {
                Id = template.Id,
                TemplateName = template.TemplateName,
                HtmlContent = template.HtmlContent,
                TemplateType = template.TemplateType
            };

            // Return the Edit view with the populated view model.
            return View(viewModel);
        }

        // POST: Template/Edit/{id}
        // Processes the form submission for editing an invoice template.
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Edit(InvoiceTemplateViewModel model)
        {
            // Check if the submitted data is valid.
            if (ModelState.IsValid)
            {
                // Find the existing template by its id.
                var template = await _context.Templates.FindAsync(model.Id);
                if (template == null)
                {
                    return NotFound();
                }

                // Update template properties with values from the view model.
                template.TemplateName = model.TemplateName;
                template.HtmlContent = model.HtmlContent;
                template.TemplateType = model.TemplateType;
                template.UpdatedDate = DateTime.Now;

                // Set the updater; default to "Admin" if no identity is available.
                template.UpdatedBy = User.Identity?.Name ?? "Admin";

                // Save changes asynchronously.
                await _context.SaveChangesAsync();

                // Redirect to the Index action to show the updated list of templates.
                return RedirectToAction(nameof(Index));
            }
            // If the data is invalid, redisplay the form with validation errors.
            return View(model);
        }

        // GET: Template/Delete/{id}
        // Displays a confirmation page to delete an invoice template.
        public async Task<IActionResult> Delete(int? id)
        {
            // Check if the id is provided; if not, return 404.
            if (id == null)
            {
                return NotFound();
            }

            // Retrieve the template without tracking (for display only).
            var template = await _context.Templates.AsNoTracking()
                .FirstOrDefaultAsync(t => t.Id == id);

            // If the template is not found, return 404.
            if (template == null)
            {
                return NotFound();
            }

            // Return the Delete view with the template details.
            return View(template);
        }

        // POST: Template/Delete/{id}
        // Processes the deletion of an invoice template.
        [HttpPost, ActionName("Delete")]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> DeleteConfirmed(int id)
        {
            // Find the template by id.
            var template = await _context.Templates.FindAsync(id);
            if (template == null)
            {
                return NotFound();
            }

            // Remove the template from the context.
            _context.Templates.Remove(template); 

            // Save changes asynchronously.
            await _context.SaveChangesAsync();

            // Redirect back to the Index view.
            return RedirectToAction(nameof(Index));
        }
    }
}
Index Action:
  • This function retrieves a list of all invoice templates using AsNoTracking() for performance and passes them to the Index view.
Create Actions (GET and POST):
  • The GET action returns a new instance of InvoiceTemplateViewModel so that the form fields are empty.
  • The POST action validates the submitted model, maps it to a domain model (Template), and saves it. If the model is invalid, it returns the form with errors.
Edit Actions (GET and POST):
  • The GET action retrieves an existing template by its ID (without tracking) and maps it to the view model.
  • The POST action validates the submitted model, finds the corresponding domain model, updates its properties, and saves the changes.
Delete Actions (GET and POST):
  • The GET action retrieves the template to display a deletion confirmation page.
  • The POST action (DeleteConfirmed) removes the template from the database and then redirects to the Index view.
Views

Create the following Razor views under the Views/Template folder.

Index.cshtml

Create a view named Index.cshtml within the Views/Template folder and then copy and paste the following code. This view displays a list of all invoice templates in a table within a responsive Bootstrap card. It shows key information such as the template name, type, created date, and creator.

@model IEnumerable<OrderInvoiceSystem.Models.Template>
@{
    ViewData["Title"] = "Invoice Templates";
}

<div class="container mt-5">
    <div class="card shadow-sm">
        <div class="card-header bg-primary text-white">
            <h2 class="mb-0">@ViewData["Title"]</h2>
        </div>
        <div class="card-body">
            <div class="mb-3 text-end">
                <a asp-action="Create" class="btn btn-success">
                    <i class="bi bi-plus-circle"></i> Create New Template
                </a>
            </div>
            <div class="table-responsive">
                <table class="table table-hover">
                    <thead class="table-light">
                        <tr>
                            <th>Template Name</th>
                            <th>Type</th>
                            <th>Created Date</th>
                            <th>Created By</th>
                            <th class="text-center">Actions</th>
                        </tr>
                    </thead>
                    <tbody>
                        @foreach (var template in Model)
                        {
                            <tr>
                                <td>@template.TemplateName</td>
                                <td>@template.TemplateType</td>
                                <td>@template.CreatedDate.ToString("g")</td>
                                <td>@template.CreatedBy</td>
                                <td class="text-center">
                                    <a asp-action="Edit" asp-route-id="@template.Id" class="btn btn-sm btn-primary me-1">
                                        <i class="bi bi-pencil-square"></i> Edit
                                    </a>
                                    <a asp-action="Delete" asp-route-id="@template.Id" class="btn btn-sm btn-danger">
                                        <i class="bi bi-trash"></i> Delete
                                    </a>
                                </td>
                            </tr>
                        }
                    </tbody>
                </table>
            </div>
        </div>
    </div>
</div>
Create.cshtml

Create a view named Create.cshtml within the Views/Template folder, and then copy and paste the following code. This view renders a form that allows an admin to create a new invoice template. It uses the InvoiceTemplateViewModel for model binding.

@model OrderInvoiceSystem.ViewModels.InvoiceTemplateViewModel

@{
    ViewData["Title"] = "Create Invoice Template";
}

<div class="container my-5">
    <div class="row">
        <!-- Left Column: Form & Editor -->
        <div class="col-md-7">
            <div class="card shadow">
                <div class="card-header bg-primary text-white">
                    <h3 class="card-title mb-0">@ViewData["Title"]</h3>
                </div>
                <div class="card-body">
                    <form asp-action="Create" method="post" novalidate>
                        <!-- Template Name -->
                        <div class="mb-3">
                            <label asp-for="TemplateName" class="form-label fw-bold"></label>
                            <input asp-for="TemplateName" class="form-control" placeholder="e.g., Order Completion Template" />
                            <span asp-validation-for="TemplateName" class="text-danger"></span>
                        </div>

                        <!-- Template Type -->
                        <div class="mb-3">
                            <label asp-for="TemplateType" class="form-label fw-bold"></label>
                            <select asp-for="TemplateType" class="form-select"
                                    asp-items="Html.GetEnumSelectList<OrderInvoiceSystem.Models.TemplateType>()">
                                <option value="">-- Select Template Type --</option>
                            </select>
                            <span asp-validation-for="TemplateType" class="text-danger"></span>
                        </div>

                        <!-- CKEditor: HTML Content -->
                        <div class="mb-3">
                            <label asp-for="HtmlContent" class="form-label fw-bold"></label>
                            <textarea asp-for="HtmlContent" id="HtmlContent" class="form-control" rows="10"></textarea>
                            <span asp-validation-for="HtmlContent" class="text-danger"></span>
                        </div>

                        <div class="d-grid">
                            <button type="submit" class="btn btn-success btn-lg">Save Template</button>
                        </div>
                    </form>
                </div>
            </div>
        </div>

        <!-- Right Column: Allowed Placeholders -->
        <div class="col-md-5">
            <div class="card shadow-sm">
                <div class="card-header bg-info text-white text-center py-3">
                    <h3 class="mb-0">Allowed Placeholders</h3>
                </div>
                <div class="card-body">
                    <p>Use the following placeholders in your HTML template to dynamically insert order data:</p>
                    <ul>
                        <li><code>{CustomerName}</code> – Customer's full name.</li>
                        <li><code>{OrderDate}</code> – Formatted order date.</li>
                        <li><code>{OrderNumber}</code> – Unique order number.</li>
                        <li><code>{OrderStatus}</code> – Current order status (Pending, Completed, Cancelled).</li>
                        <li><code>{ProductList}</code> – List of ordered products.</li>
                        <li><code>{TotalAmount}</code> – Total order amount in currency format.</li>
                        <li><code>{PaymentStatus}</code> – Payment status (for Payment Success templates).</li>
                        <li><code>{PaymentDate}</code> – Payment date (for Payment Success templates).</li>
                        <li><code>{PaymentMethod}</code> – Payment method used (for Payment Success templates).</li>
                    </ul>
                    <p class="text-muted">
                        Please ensure you use these placeholders exactly as shown (including curly braces) so they can be properly replaced when the invoice is generated.
                    </p>
                </div>
            </div>
        </div>
    </div>
</div>

@section Scripts {
    <!-- Include CKEditor via CDN -->
    <script src="https://cdn.ckeditor.com/ckeditor5/35.3.1/classic/ckeditor.js"></script>

    <script>
        // Initialize CKEditor on the textarea with id 'HtmlContent'
        CKEDITOR.replace('HtmlContent');
    </script>
}
Edit.cshtml

Create a view named Edit.cshtml within the Views/Template folder and then copy and paste the following code. This view allows an admin to edit an existing invoice template. It is similar to the Create view but pre-populates the form with existing data.

@model OrderInvoiceSystem.ViewModels.InvoiceTemplateViewModel

@{
    ViewData["Title"] = "Edit Invoice Template";
}

<div class="container my-5">
    <div class="row">
        <!-- Left Column: Edit Form & CKEditor -->
        <div class="col-md-7">
            <div class="card shadow">
                <div class="card-header bg-primary text-white">
                    <h3 class="card-title mb-0">@ViewData["Title"]</h3>
                </div>
                <div class="card-body">
                    <form asp-action="Edit" method="post" novalidate>
                        <input type="hidden" asp-for="Id" />

                        <!-- Template Name -->
                        <div class="mb-3">
                            <label asp-for="TemplateName" class="form-label fw-bold"></label>
                            <input asp-for="TemplateName" class="form-control" placeholder="e.g., Order Completion Template" />
                            <span asp-validation-for="TemplateName" class="text-danger"></span>
                        </div>

                        <!-- Template Type -->
                        <div class="mb-3">
                            <label asp-for="TemplateType" class="form-label fw-bold"></label>
                            <select asp-for="TemplateType"
                                    class="form-select"
                                    asp-items="Html.GetEnumSelectList<OrderInvoiceSystem.Models.TemplateType>()">
                                <option value="">-- Select Template Type --</option>
                            </select>
                            <span asp-validation-for="TemplateType" class="text-danger"></span>
                        </div>

                        <!-- CKEditor: HTML Content -->
                        <div class="mb-3">
                            <label asp-for="HtmlContent" class="form-label fw-bold"></label>
                            <textarea asp-for="HtmlContent" id="HtmlContent" class="form-control" rows="10"></textarea>
                            <span asp-validation-for="HtmlContent" class="text-danger"></span>
                        </div>

                        <!-- Update Template Button -->
                        <div class="d-grid">
                            <button type="submit" class="btn btn-success btn-lg">Update Template</button>
                        </div>
                    </form>
                </div>
            </div>
        </div>

        <!-- Right Column: Allowed Placeholders -->
        <div class="col-md-5">
            <div class="card shadow-sm">
                <div class="card-header bg-info text-white text-center py-3">
                    <h3 class="mb-0">Allowed Placeholders</h3>
                </div>
                <div class="card-body">
                    <p>Use the following placeholders in your HTML template to dynamically insert order data:</p>
                    <ul>
                        <li><code>{CustomerName}</code> – Customer's full name.</li>
                        <li><code>{OrderDate}</code> – Formatted order date.</li>
                        <li><code>{OrderNumber}</code> – Unique order number.</li>
                        <li><code>{OrderStatus}</code> – Current order status (Pending, Completed, Cancelled).</li>
                        <li><code>{ProductList}</code> – List of ordered products.</li>
                        <li><code>{TotalAmount}</code> – Total order amount in currency format.</li>
                        <li><code>{PaymentStatus}</code> – Payment status (for Payment Success templates).</li>
                        <li><code>{PaymentDate}</code> – Payment date (for Payment Success templates).</li>
                        <li><code>{PaymentMethod}</code> – Payment method used (for Payment Success templates).</li>
                    </ul>
                    <p class="text-muted">
                        Please ensure you use these placeholders exactly as shown (including curly braces) so they can be properly replaced when the invoice is generated.
                    </p>
                </div>
            </div>
        </div>
    </div>
</div>

@section Scripts {
    <!-- Include CKEditor via CDN -->
    <script src="https://cdn.ckeditor.com/ckeditor5/35.3.1/classic/ckeditor.js"></script>

    <script>
        // Initialize CKEditor on the textarea with id 'HtmlContent'
        CKEDITOR.replace('HtmlContent');
    </script>
}
Delete.cshtml View

Create a view named Delete.cshtml within the Views/Template folder, then copy and paste the following code. This view displays a confirmation page for deleting an invoice template. It shows the template details (name, type, created date, and created by) to help the admin verify that they are deleting the correct record.

@model OrderInvoiceSystem.Models.Template
@{
    ViewData["Title"] = "Delete Template";
}

<div class="container mt-5">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card shadow-sm">
                <div class="card-header bg-danger text-white text-center py-3">
                    <h2 class="mb-0">Delete Template</h2>
                </div>
                <div class="card-body">
                    <h4 class="card-title text-center mb-4">Are you sure you want to delete this template? This action cannot be undone.</h4>
                    <dl class="row">
                        <dt class="col-sm-4">Template Name:</dt>
                        <dd class="col-sm-8">@Model.TemplateName</dd>
                        <dt class="col-sm-4">Template Type:</dt>
                        <dd class="col-sm-8">@Model.TemplateType</dd>
                        <dt class="col-sm-4">Created Date:</dt>
                        <dd class="col-sm-8">@Model.CreatedDate.ToString("g")</dd>
                        <dt class="col-sm-4">Created By:</dt>
                        <dd class="col-sm-8">@Model.CreatedBy</dd>
                    </dl>
                </div>
                <div class="card-footer text-center">
                    <form asp-action="Delete" method="post" class="d-inline">
                        <input type="hidden" asp-for="Id" />
                        <button type="submit" class="btn btn-danger">Delete</button>
                    </form>
                    <a asp-action="Index" class="btn btn-secondary ms-2">Cancel</a>
                </div>
            </div>
        </div>
    </div>
</div>
Modifying _Layout.cshtml:

Please modify the _Layout.cshtml view as follows. The layout view serves as the master page for your application. It defines the overall HTML structure (header, footer, navigation) that is shared across multiple pages.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - OrderInvoiceSystem</title>
    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
    <!-- Site CSS -->
    <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
    <link rel="stylesheet" href="~/OrderInvoiceSystem.styles.css" asp-append-version="true" />
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/bootstrap-icons.css" />
</head>
<body>
    <header>
        <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
            <div class="container-fluid">
                <a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">OrderInvoiceSystem</a>
                <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse"
                        aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
                    <ul class="navbar-nav flex-grow-1">
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
                        </li>
                        @* Show Create Order and My Orders link for authenticated users *@
                        @if (User.Identity != null && User.Identity.IsAuthenticated)
                        {
                            <li class="nav-item">
                                <a class="nav-link text-dark" asp-area="" asp-controller="Order" asp-action="Create">Create Order</a>
                            </li>
                            <li class="nav-item">
                                <a class="nav-link text-dark" asp-area="" asp-controller="Order" asp-action="Index">My Orders</a>
                            </li>
                        }
                        @* If user is admin (using email claim), provide a link to manage templates *@
                        @if (User.Identity != null && User.Identity.IsAuthenticated && User.HasClaim(c => c.Type == System.Security.Claims.ClaimTypes.Email && c.Value == "admin@example.com"))
                        {
                            <li class="nav-item">
                                <a class="nav-link text-dark" asp-area="" asp-controller="Template" asp-action="Index">Manage Templates</a>
                            </li>
                        }
                    </ul>
                    <ul class="navbar-nav">
                        @if (User.Identity != null && User.Identity.IsAuthenticated)
                        {
                            <li class="nav-item">
                                <a class="nav-link text-dark" asp-area="" asp-controller="Account" asp-action="Logout">Logout</a>
                            </li>
                        }
                        else
                        {
                            <li class="nav-item">
                                <a class="nav-link text-dark" asp-area="" asp-controller="Account" asp-action="Login">Login</a>
                            </li>
                            <li class="nav-item">
                                <a class="nav-link text-dark" asp-area="" asp-controller="Account" asp-action="Register">Register</a>
                            </li>
                        }
                    </ul>
                </div>
            </div>
        </nav>
    </header>
    <div class="container">
        <main role="main" class="pb-3">
            @RenderBody()
        </main>
    </div>
    <footer class="border-top footer text-muted">
        <div class="container">
            &copy; 2025 - OrderInvoiceSystem - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
        </div>
    </footer>

    <!-- jQuery (required for jQuery validation plugins) -->
    <script src="~/lib/jquery/dist/jquery.min.js"></script>
    <!-- Bootstrap Bundle JS (includes Popper) -->
    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
    <!-- jQuery Validation -->
    <script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
    <script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
    <!-- Site JS -->
    <script src="~/js/site.js" asp-append-version="true"></script>
    @await RenderSectionAsync("Scripts", required: false)
    @RenderSection("Styles", required: false)
</body>
</html>
Creating HTML Templates:

Now, run the application, log in using the Admin user, and create the templates. Please use the below HTML for Creating the Templates:

Order Completion Template:
<html>
  <body style="font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin:0; padding:0; background:#f7f7f7;">
    <div style="max-width:800px; margin:30px auto; background:#ffffff; border:1px solid #e0e0e0; box-shadow:0 0 10px rgba(0,0,0,0.1);">
      <!-- Header -->
      <div style="padding:20px; border-bottom:1px solid #e0e0e0; text-align:center;">
        <h1 style="margin:0; font-size:28px; color:#007bff;">Invoice</h1>
        <p style="margin:5px 0; font-size:16px; color:#007bff;">Order Completed</p>
      </div>
      <!-- Order Details -->
      <div style="padding:20px;">
        <table style="width:100%; margin-bottom:20px;">
          <tr>
            <td style="font-size:16px; padding:5px 0; width:35%;"><strong>Customer Name:</strong></td>
            <td style="font-size:16px; padding:5px 0;">{CustomerName}</td>
          </tr>
          <tr>
            <td style="font-size:16px; padding:5px 0;"><strong>Order Date:</strong></td>
            <td style="font-size:16px; padding:5px 0;">{OrderDate}</td>
          </tr>
          <tr>
            <td style="font-size:16px; padding:5px 0;"><strong>Order Number:</strong></td>
            <td style="font-size:16px; padding:5px 0;">{OrderNumber}</td>
          </tr>
          <tr>
            <td style="font-size:16px; padding:5px 0;"><strong>Status:</strong></td>
            <td style="font-size:16px; padding:5px 0;">{OrderStatus}</td>
          </tr>
        </table>
        <!-- Product Details -->
        <table style="width:100%; border-collapse:collapse;">
          <thead>
            <tr style="background:#f2f2f2;">
              <th style="padding:12px; border:1px solid #ddd; text-align:left;">Product</th>
              <th style="padding:12px; border:1px solid #ddd; text-align:left;">Quantity</th>
              <th style="padding:12px; border:1px solid #ddd; text-align:left;">Unit Price</th>
              <th style="padding:12px; border:1px solid #ddd; text-align:left;">Amount</th>
            </tr>
          </thead>
          <tbody>
            {ProductList}
          </tbody>
        </table>
        <!-- Total -->
        <p style="text-align:right; font-size:20px; font-weight:bold; color:#007bff; margin-top:20px;">Total: {TotalAmount}</p>
      </div>
      <!-- Footer -->
      <div style="padding:15px; border-top:1px solid #e0e0e0; text-align:center; font-size:14px; color:#777;">
        <p style="margin:0;">Thank you for your business!</p>
        <p style="margin:0;">My Estore App</p>
      </div>
    </div>
  </body>
</html>
Order Cancelled Template
<html>
  <body style="font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin:0; padding:0; background:#f7f7f7;">
    <div style="max-width:800px; margin:30px auto; background:#ffffff; border:1px solid #e0e0e0; box-shadow:0 0 10px rgba(0,0,0,0.1);">
      <!-- Header -->
      <div style="padding:20px; border-bottom:1px solid #e0e0e0; text-align:center;">
        <h1 style="margin:0; font-size:28px; color:#dc3545;">Invoice</h1>
        <p style="margin:5px 0; font-size:16px; color:#dc3545;">Order Cancelled</p>
      </div>
      <!-- Order Details -->
      <div style="padding:20px;">
        <table style="width:100%; margin-bottom:20px;">
          <tr>
            <td style="font-size:16px; padding:5px 0; width:35%;"><strong>Customer Name:</strong></td>
            <td style="font-size:16px; padding:5px 0;">{CustomerName}</td>
          </tr>
          <tr>
            <td style="font-size:16px; padding:5px 0;"><strong>Order Date:</strong></td>
            <td style="font-size:16px; padding:5px 0;">{OrderDate}</td>
          </tr>
          <tr>
            <td style="font-size:16px; padding:5px 0;"><strong>Order Number:</strong></td>
            <td style="font-size:16px; padding:5px 0;">{OrderNumber}</td>
          </tr>
          <tr>
            <td style="font-size:16px; padding:5px 0;"><strong>Status:</strong></td>
            <td style="font-size:16px; padding:5px 0;">{OrderStatus}</td>
          </tr>
        </table>
        <!-- Product Details -->
        <table style="width:100%; border-collapse:collapse;">
          <thead>
            <tr style="background:#f2f2f2;">
              <th style="padding:12px; border:1px solid #ddd; text-align:left;">Product</th>
              <th style="padding:12px; border:1px solid #ddd; text-align:left;">Quantity</th>
              <th style="padding:12px; border:1px solid #ddd; text-align:left;">Unit Price</th>
              <th style="padding:12px; border:1px solid #ddd; text-align:left;">Amount</th>
            </tr>
          </thead>
          <tbody>
            {ProductList}
          </tbody>
        </table>
        <!-- Total -->
        <p style="text-align:right; font-size:20px; font-weight:bold; color:#dc3545; margin-top:20px;">Total: {TotalAmount}</p>
      </div>
      <!-- Footer -->
      <div style="padding:15px; border-top:1px solid #e0e0e0; text-align:center; font-size:14px; color:#777;">
        <p style="margin:0;">Your order has been cancelled.</p>
        <p style="margin:0;">Please contact our support for further assistance.</p>
        <p style="margin:0;">My Estore App</p>
      </div>
    </div>
  </body>
</html>
Order Pending Template:
<html>
  <body style="font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin:0; padding:0; background:#f7f7f7;">
    <div style="max-width:800px; margin:30px auto; background:#ffffff; border:1px solid #e0e0e0; box-shadow:0 0 10px rgba(0,0,0,0.1);">
      <!-- Header -->
      <div style="padding:20px; border-bottom:1px solid #e0e0e0; text-align:center;">
        <h1 style="margin:0; font-size:28px; color:#ffc107;">Invoice</h1>
        <p style="margin:5px 0; font-size:16px; color:#ffc107;">Order Pending</p>
      </div>
      <!-- Order Details -->
      <div style="padding:20px;">
        <table style="width:100%; margin-bottom:20px;">
          <tr>
            <td style="font-size:16px; padding:5px 0; width:35%;"><strong>Customer Name:</strong></td>
            <td style="font-size:16px; padding:5px 0;">{CustomerName}</td>
          </tr>
          <tr>
            <td style="font-size:16px; padding:5px 0;"><strong>Order Date:</strong></td>
            <td style="font-size:16px; padding:5px 0;">{OrderDate}</td>
          </tr>
          <tr>
            <td style="font-size:16px; padding:5px 0;"><strong>Order Number:</strong></td>
            <td style="font-size:16px; padding:5px 0;">{OrderNumber}</td>
          </tr>
          <tr>
            <td style="font-size:16px; padding:5px 0;"><strong>Status:</strong></td>
            <td style="font-size:16px; padding:5px 0;">{OrderStatus}</td>
          </tr>
        </table>
        <!-- Product Details -->
        <table style="width:100%; border-collapse:collapse;">
          <thead>
            <tr style="background:#f2f2f2;">
              <th style="padding:12px; border:1px solid #ddd; text-align:left;">Product</th>
              <th style="padding:12px; border:1px solid #ddd; text-align:left;">Quantity</th>
              <th style="padding:12px; border:1px solid #ddd; text-align:left;">Unit Price</th>
              <th style="padding:12px; border:1px solid #ddd; text-align:left;">Amount</th>
            </tr>
          </thead>
          <tbody>
            {ProductList}
          </tbody>
        </table>
        <!-- Total -->
        <p style="text-align:right; font-size:20px; font-weight:bold; color:#ffc107; margin-top:20px;">Total: {TotalAmount}</p>
      </div>
      <!-- Footer -->
      <div style="padding:15px; border-top:1px solid #e0e0e0; text-align:center; font-size:14px; color:#777;">
        <p style="margin:0;">Your order is pending payment confirmation.</p>
        <p style="margin:0;">Please complete the payment to process your order.</p>
        <p style="margin:0;">My Estore App</p>
      </div>
    </div>
  </body>
</html>
Placing Order:

Now, run the application, log in using the Customer User, and place an order. It should work as expected.

The primary objective of the above project is to provide a robust platform that automates the complete order-to-invoice lifecycle. It achieves this by integrating order management, dynamic PDF invoice generation, and customer communication, all within a secure, scalable, and customizable ASP.NET Core MVC framework. This system is built to serve as a foundation for e-commerce platforms looking to streamline their operational workflow and enhance the customer experience through automated, professional invoice delivery.

In the next article, I will discuss developing a Food Delivery App using ASP.NET Core MVC and Entity Framework Core. I hope you enjoy this in-depth article on Real-time Ecommerce Application Development using ASP.NET Core MVC. Please give your valuable feedback about this e-commerce application using the ASP.NET Core MVC article and tell me how we can improve this project.

Leave a Reply

Your email address will not be published. Required fields are marked *