Part 15: Online Payment Gateway & Cash on Delivery | ASP.NET Core MVC Full Project

 


Part 15: Online Payment Gateway, Cash on Delivery & Order Checkout

Step 1: Configuring External App Secret Providers

The appsettings.json file handles your environmental configuration keys. This block initializes the three major external integration points your commercial pipeline requires: secure payments, OAuth identification routing, and background messaging engines.

JSON / appsettings.json
"Stripe": {
  "PublishableKey": "pk_test_your_publishable_key",
  "SecretKey": "sk_test_your_secret_key"
},
"EmailSettings": {
  "SmtpServer": "smtp.gmail.com",
  "SmtpPort": 587,
  "SenderEmail": "your-email@gmail.com",
  "SenderPassword": "your-app-password"
},

Core Configuration Sections Breakdown

  • The Stripe Payment Portal Block (Stripe): This section holds your payment gateway access tokens.

    • The PublishableKey (pk_test_...) is completely safe to expose to the public browser view. It is used on the client side by Stripe's JavaScript library to tokenize card details directly on Stripe's servers.

    • The SecretKey (sk_test_...) must remain strictly hidden on the server side. Your C# controllers use it to securely call Stripe's APIs and capture funds from a transaction.

  • The Background Notification SMTP Engine (EmailSettings): This section establishes a connection to a secure Mail Transfer Protocol (SMTP) server. By mapping out variables like SmtpServer and SmtpPort (using port 587 for standard secure TLS encryption), your application can send automated notifications—such as order confirmations or invoice receipts—directly to your buyers.

Step 2: Exposing the Public Stripe Key to the Frontend

In this step, you are using the ViewBag dictionary inside your CheckoutController's Index action to read the public configuration value and send it straight to your user interface.

C# / Controllers/CheckoutController.cs (ViewBag Mapping)
ViewBag.StripePublishableKey = _configuration["Stripe:PublishableKey"];

Core Mechanism & Architectural Breakdown

  • Direct Key Index Resolution (_configuration["Stripe:PublishableKey"]): The _configuration object (which you injected into your controller constructor back in Part 14, Step 2) reads your JSON application files. By passing the colon-separated path string "Stripe:PublishableKey", the configuration framework instantly traverses into your appsettings.json file, looks up the Stripe parent section, and extracts the test key literal string.

  • Dynamic Data Transfer Wrapper (ViewBag): ViewBag is a dynamic property wrapper that allows you to pass temporary data from a controller to a view without needing to modify your main strongly typed object structure (CheckoutViewModel). Creating the custom property .StripePublishableKey allows the value to be carried over seamlessly during page compilation.

  • Strict Architectural Separation of Concerns (SoC): Notice what is happening here from a security engineering standpoint: you are only exposing the PublishableKey. Your sensitive SecretKey remains locked on the server side where the frontend browser can never see it. This is a crucial security pattern that satisfies standard payment card industry security regulations.

Step 3: Implementing the Client-Side Payment Gateway Scripts

By wrapping this code inside the @section Scripts directive, you ensure that the checkout logic is loaded at the bottom of the rendered page body. This preserves optimal page-loading performance while preventing conflicts with your master layout dependencies.

Razor / Views/Checkout/Index.cshtml (Scripts Section)
@section Scripts {
    <partial name="_ValidationScriptsPartial" />
    
    @if (!string.IsNullOrEmpty(ViewBag.StripePublishableKey))
    {
        <script src="https://js.stripe.com/v3/"></script>
        <script>
            var stripe = Stripe('@ViewBag.StripePublishableKey');
            var elements = stripe.elements();
            var card = elements.create('card');
            card.mount('#cardElement');

            card.on('change', function(event) {
                var displayError = document.getElementById('cardErrors');
                if (event.error) {
                    displayError.textContent = event.error.message;
                } else {
                    displayError.textContent = '';
                }
            });

            document.querySelectorAll('input[name="PaymentMethod"]').forEach(function(radio) {
                radio.addEventListener('change', function() {
                    document.getElementById('stripePayment').style.display = 
                        this.value === 'Stripe' ? 'block' : 'none';
                });
            });

            document.getElementById('checkoutForm').addEventListener('submit', function(event) {
                var paymentMethod = document.querySelector('input[name="PaymentMethod"]:checked').value;
                if (paymentMethod === 'Stripe') {
                    event.preventDefault();
                    stripe.createToken(card).then(function(result) {
                        if (result.error) {
                            document.getElementById('cardErrors').textContent = result.error.message;
                        } else {
                            var hiddenInput = document.createElement('input');
                            hiddenInput.setAttribute('type', 'hidden');
                            hiddenInput.setAttribute('name', 'StripeToken');
                            hiddenInput.setAttribute('value', result.token.id);
                            document.getElementById('checkoutForm').appendChild(hiddenInput);
                            document.getElementById('checkoutForm').submit();
                        }
                    });
                }
            });
        </script>
    }
}

Core JavaScript & Integration Logic Breakdown

  • Defensive Gateway Script Loading:

    @if (!string.IsNullOrEmpty(ViewBag.StripePublishableKey))

    This server-side Razor conditional protects your page stability. The external Stripe script (js.stripe.com/v3/) and setup configurations only embed into the document if your controller successfully loads a valid key string from appsettings.json, preventing runtime browser reference errors.

  • Secure Element Embedding (card.mount('#cardElement')): This single line provides industry-standard security. Instead of rendering custom, unsecure input text boxes for credit card numbers, expirations, and CVC codes inside your application, Stripe creates an isolated iframe component inside your #cardElement div wrapper. The user's sensitive digits are written directly onto secure remote cloud servers, keeping your local servers out of compliance risks.

  • Conditional Form Visual Router: The querySelectorAll('input[name="PaymentMethod"]') block sets up real-time layout event listeners. Whenever a customer changes their payment choice radio button, the callback script checks the active selection value. If it matches 'Stripe', it flips the target interface layer to display: 'block'; otherwise, it hides it immediately via 'none'.

  • Form Hijacking and Secure Token Interception: When a customer clicks "Place Order," the submit handler evaluates the active payment choice. If Stripe is active, event.preventDefault() halts standard form processing. The script calls stripe.createToken(card) to convert raw card details into a single-use token signature identifier (result.token.id).

    Once successfully created, a hidden input field named StripeToken is created on the fly and appended to the document object model, and the form is securely sent along to your server backend controller.

Step 4: Implementing the Email Service Blueprint and Infrastructure

This codebase handles building custom, clean HTML transactional notification layouts dynamically and dispatching them securely through an external mail transport layer.

C# / Services/IEmailService.cs
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;

namespace MobileShop.Services
{
    public interface IEmailService
    {
        Task SendEmailAsync(string to, string subject, string body, bool isHtml = true);
        Task SendOrderConfirmationAsync(string to, string orderNumber, decimal total);
        Task SendOrderStatusUpdateAsync(string to, string orderNumber, string status);
    }

    public class EmailService : IEmailService
    {
        private readonly IConfiguration _configuration;
        private readonly ILogger<EmailService> _logger;

        public EmailService(IConfiguration configuration, ILogger<EmailService> logger)
        {
            _configuration = configuration;
            _logger = logger;
        }

        public async Task SendEmailAsync(string to, string subject, string body, bool isHtml = true)
        {
            try
            {
                var smtpServer = _configuration["EmailSettings:SmtpServer"];
                var smtpPort = int.Parse(_configuration["EmailSettings:SmtpPort"] ?? "587");
                var senderEmail = _configuration["EmailSettings:SenderEmail"];
                var senderPassword = _configuration["EmailSettings:SenderPassword"];

                using var client = new System.Net.Mail.SmtpClient(smtpServer, smtpPort)
                {
                    EnableSsl = true,
                   UseDefaultCredentials = false, Credentials = new System.Net.NetworkCredential(senderEmail, senderPassword) }; var message = new System.Net.Mail.MailMessage(senderEmail, to, subject, body) { IsBodyHtml = isHtml }; await client.SendMailAsync(message); _logger.LogInformation($"Email sent to {to}"); } catch (Exception ex) { _logger.LogError($"Failed to send email: {ex.Message}"); } } public async Task SendOrderConfirmationAsync(string to, string orderNumber, decimal total) { var subject = $"Order Confirmation - {orderNumber}"; var body = $@" <h2>Thank you for your order!</h2> <p>Your order <strong>{orderNumber}</strong> has been placed successfully.</p> <p>Total Amount: <strong>${total:N2}</strong></p> <p>We will notify you once your order is shipped.</p>"; await SendEmailAsync(to, subject, body); } public async Task SendOrderStatusUpdateAsync(string to, string orderNumber, string status) { var subject = $"Order Status Update - {orderNumber}"; var body = $@" <h2>Order Status Update</h2> <p>Your order <strong>{orderNumber}</strong> status has been updated to: <strong>{status}</strong></p> <p>You can track your order on our website.</p>"; await SendEmailAsync(to, subject, body); } } }

Core Interface & Implementation Breakdown

  • The Dependency Abstraction Contract (IEmailService): The interface maps out clear, domain-specific operations (SendOrderConfirmationAsync and SendOrderStatusUpdateAsync) alongside a raw base processor (SendEmailAsync). This hides complex low-level SMTP routing properties from your business workflows.

  • Dynamic Configuration Mining:

    var smtpServer = _configuration["EmailSettings:SmtpServer"];
    var smtpPort = int.Parse(_configuration["EmailSettings:SmtpPort"] ?? "587");
    

    The code safely fetches network routing targets directly from the appsettings.json keys configured back in Step 1. Using a null-coalescing default operator (?? "587") prevents runtime exceptions if a port setting is missing, falling back to standard secure TLS lines automatically.

  • Secure Networking with SMTP Lifecycle Scoping: By instantiating SmtpClient with the C# using var declaration syntax, you ensure that the active TCP networking socket is instantly torn down and disposed of cleanly from memory once the transmission completes. Setting EnableSsl = true forces strong cryptographic encryption across the transmission wire, safely protecting your sender credentials.

  • Asynchronous Execution Block Isolation (try-catch): E-commerce order completions should never crash or freeze just because an external mail server experiences a brief timeout delay. Wrapping the logic in an asynchronous try-catch container with targeted diagnostics (_logger.LogError) ensures that network execution failures are recorded silently while the checkout flow finishes safely.

  • Polished HTML String Interpolation Templates: Using multi-line C# verbatim string blocks ($@"...") allows you to write clean, maintainable HTML design structures straight inside your method logic. The code passes raw pricing data types directly through standard numeric text formatters ({total:N2}) to guarantee a clean appearance across all client mail applications.

Step 5: Wiring Services and Middleware pipeline inside Program.cs

This code block modifies both the Dependency Injection Container (before builder.Build()) and the HTTP Request Pipeline (after builder.Build()).

C# / Program.cs (Service Configuration)
// Stripe Configuration
builder.Services.Configure<StripeSettings>(builder.Configuration.GetSection("Stripe"));

// Custom Services
builder.Services.AddScoped<IEmailService, EmailService>();
.
.
.
app.UseSession();

StripeConfiguration.ApiKey = builder.Configuration["Stripe:SecretKey"];

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

Core Service Registrations & Middleware Breakdown

  • Strongly Typed Options Mapping:

    builder.Services.Configure<StripeSettings>(builder.Configuration.GetSection("Stripe"));

    Instead of manually reading configuration strings inside every controller or class, this pattern binds your "Stripe" JSON section from appsettings.json directly into a strongly typed StripeSettings class object. This configuration can now be cleanly injected into any constructor using the standard IOptions<StripeSettings> interface pattern.

  • Scoped Mail Service Lifetime Registration:

    builder.Services.AddScoped<IEmailService, EmailService>();

    Registering your EmailService as Scoped means a new, isolated instance of the mail service will be created for each incoming web request (e.g., when a user submits an order) and destroyed cleanly once that request completes. This ensures proper memory management and protects data isolation between simultaneous shoppers.

  • Global SDK Initialization & Secret Authorization:

    StripeConfiguration.ApiKey = builder.Configuration["Stripe:SecretKey"];

    This is a critical initialization pattern for the Stripe C# SDK. By setting the static StripeConfiguration.ApiKey property directly during application startup, you establish a universal, secure validation token. Every backend API call your application makes from this point forward will be automatically signed with your secret key, allowing your store to create charges safely.

  • Middleware Ordering Pipeline (app.UseSession()): Placing your middleware components in the correct sequence is highly important. app.UseSession() must be executed before authentication, authorization, and route processing blocks. This ensures that session-state memory pools are fully loaded into the request context before any user verification or cart deduction checks run.

Step 6: Explaining the Concrete IOrderService Implementation

This service coordinates interactions with your ApplicationDbContext to ensure that data transitions smoothly from temporary cart vectors to permanent database schemas.

C# / Services/IOrderService.cs
namespace MobileShop.Services
{
    public interface IOrderService
    {
        Task<Order> CreateOrderAsync(CheckoutViewModel model, string? userId);
        Task<Order?> GetOrderByNumberAsync(string orderNumber);
        Task<bool> UpdateOrderStatusAsync(int orderId, OrderStatus status); Task<bool> ProcessPaymentAsync(int orderId, string transactionId); } public class OrderService : IOrderService { private readonly ApplicationDbContext _context; private readonly ILogger<OrderService> _logger; public OrderService(ApplicationDbContext context, ILogger<OrderService> logger) { _context = context; _logger = logger; } public async Task<Order> CreateOrderAsync(CheckoutViewModel model, string? userId) { var order = new Order { UserId = userId, Subtotal = model.Cart.CartTotal, TaxAmount = model.Cart.Tax, ShippingCost = model.Cart.Shipping, TotalAmount = model.Cart.GrandTotal, PaymentMethod = model.PaymentMethod, ShippingAddress = model.Address, ShippingCity = model.City, ShippingPostalCode = model.PostalCode, ShippingCountry = model.Country, ShippingPhone = model.Phone, Notes = model.OrderNotes }; foreach (var item in model.Cart.CartItems) { order.OrderItems.Add(new OrderItem { ProductId = item.ProductId, Quantity = item.Quantity, UnitPrice = item.UnitPrice }); // Update stock var product = await _context.Products.FindAsync(item.ProductId); if (product != null) { product.StockQuantity -= item.Quantity; } } _context.Orders.Add(order); await _context.SaveChangesAsync(); _logger.LogInformation($"Order {order.OrderNumber} created successfully"); return order; } public async Task<bool> UpdateOrderStatusAsync(int orderId, OrderStatus status) { var order = await _context.Orders.FindAsync(orderId); if (order == null) return false; order.Status = status; if (status == OrderStatus.Shipped) { order.ShippedDate = DateTime.Now; } else if (status == OrderStatus.Delivered) { order.DeliveredDate = DateTime.Now; // FIX: Mark payment as Paid for COD and manual payment methods on delivery if (order.PaymentStatus == PaymentStatus.Pending && (order.PaymentMethod == PaymentMethod.CashOnDelivery || order.PaymentMethod == PaymentMethod.UPI || order.PaymentMethod == PaymentMethod.CreditCard || order.PaymentMethod == PaymentMethod.DebitCard)) { order.PaymentStatus = PaymentStatus.Paid; } } else if (status == OrderStatus.Refunded) { // FIX: When order is refunded, payment status must also be Refunded order.PaymentStatus = PaymentStatus.Refunded; } else if (status == OrderStatus.Cancelled) { // FIX: If order was already paid and then cancelled, mark payment as Refunded if (order.PaymentStatus == PaymentStatus.Paid) { order.PaymentStatus = PaymentStatus.Refunded; } } await _context.SaveChangesAsync(); _logger.LogInformation($"Order {orderId} status updated to {status}"); return true; } public async Task<bool> ProcessPaymentAsync(int orderId, string transactionId) { var order = await _context.Orders.FindAsync(orderId); if (order == null) return false; order.PaymentStatus = PaymentStatus.Paid; order.TransactionId = transactionId; await _context.SaveChangesAsync(); _logger.LogInformation($"Payment processed for order {orderId}"); return true; }

public async Task<Order?> GetOrderByNumberAsync(string orderNumber) { return await _context.Orders .Include(o => o.OrderItems) .ThenInclude(oi => oi.Product) .Include(o => o.User) .FirstOrDefaultAsync(o => o.OrderNumber == orderNumber); } } }

1. CreateOrderAsync (Order Persistence & Inventory Control)

This method takes the submitted web form data and converts it into a permanent database transaction ledger.

  • Relational Mapping Pattern: It creates a new Order entity and populates its properties using the values passed from the CheckoutViewModel.

  • Freezing Purchase Costs (UnitPrice = item.UnitPrice): Inside the foreach loop, the script explicitly maps the current product cost down into historical individual OrderItem rows. This ensures that even if you alter a mobile phone's pricing in your catalog next month, the original purchase ledger amount remains unchanged.

  • Inline Inventory Subtraction:

    product.StockQuantity -= item.Quantity;

    Every time an item is successfully processed, Entity Framework tracks down the product record and automatically subtracts the requested quantity from your database stock. This prevents overselling out-of-stock items.

2. UpdateOrderStatusAsync (Fulfillment & Payment Synchronizer)

This state-machine engine manages the order fulfillment lifecycle (Pending, Shipped, Delivered, etc.) while applying logical state validation fixes:

  • Dynamic Timestamp Logging: Moving an order status to Shipped or Delivered automatically attaches a live server date stamp (DateTime.Now) onto your records for fulfillment metrics tracking.

  • Cash on Delivery (COD) Resolution Rules: If an order status is marked as Delivered, the system checks if the payment method was Cash on Delivery or physical banking notes. If it matches, it converts the PaymentStatus from Pending straight to Paid.

  • Refund Guard Rails: If an administrator triggers a Refunded or Cancelled action, the state machine intercepts the tracking loop to accurately update the payment status column, keeping your financial statements accurate.

3. ProcessPaymentAsync (Digital Gateway Settlement Verification)

This method acts as the callback landing strip for digital transactions (such as successful Stripe webhooks or client-side captures).

  • Transaction Signature Binding: Once an online payment provider authorizes a credit card charge, this method updates the target record's PaymentStatus to Paid and securely binds the external provider's unique payment tracking string (transactionId) straight to the order row. This ensures you can easily trace the transaction inside your Stripe Dashboard later if a dispute arises.

Step 7: Registering the Order Service in Program.cs

By adding this configuration to your application startup engine, you instruct ASP.NET Core on how to manage the lifecycle of your database order tracking logic.

Core Mechanism & Lifetime Breakdown

  • The Scoped Dependency Lifecycle Pattern:

    builder.Services.AddScoped<IOrderService, OrderService>();

    Registering this service as Scoped is an architectural best practice for e-commerce transactional workloads. It ensures that a single instance of OrderService is created per individual HTTP request context (when a customer clicks "Place Order") and shared across any components handling that request.

  • Clean Context Alignment: Because your OrderService depends directly on Entity Framework Core's ApplicationDbContext (which is registered as Scoped by default), registering your order manager as Scoped prevents severe architectural dependency mismatch issues (such as scoping errors where a transient service attempts to hold onto a disposed database pipeline context).

  • Complete Middleware Pipeline Synergy: Once this registration is dropped in, the runtime can automatically instantiate and resolve your OrderService when you pass it as a constructor dependency into your CheckoutController.

Step 8: Implementing the Stripe Payment Processing Engine

This method handles converting currency formats, requesting secure network charges via the Stripe SDK, clearing active sessions, and triggering post-purchase emails.

C# / Controllers/CheckoutController.cs (Stripe Payment Logic)
private async Task<IActionResult> ProcessStripePayment(Order order, CheckoutViewModel model)
{
    try
    {
        var options = new ChargeCreateOptions
        {
            Amount = (long)(order.TotalAmount * 100), 
            Currency = "PKR",
            Description = $"Order {order.OrderNumber}",
            Source = model.StripeToken
        };

        var service = new ChargeService();
        var charge = await service.CreateAsync(options);

        if (charge.Status == "succeeded")
        {
            await _orderService.ProcessPaymentAsync(order.Id, charge.Id);
            await _cartService.ClearCartAsync();
            await _emailService.SendOrderConfirmationAsync(model.Email, order.OrderNumber, order.TotalAmount);

            return RedirectToAction("Confirmation", new { orderNumber = order.OrderNumber });
        }
        else
        {
            TempData["Error"] = "Payment failed. Please try again.";
            return RedirectToAction("Index");
        }
    }
    catch (StripeException ex)
    {
        _logger.LogError($"Stripe error: {ex.Message}");
        TempData["Error"] = $"Payment error: {ex.Message}";
        return RedirectToAction("Index");
    }
}

Core Logic & Payment Engineering Breakdown

  • The Zero-Decimal Currency Format Requirement:

    Amount = (long)(order.TotalAmount * 100),

    This is a critical rule when integrating Stripe. Stripe processes all global transactions in smallest currency units (cents for USD, paisa for PKR) to prevent floating-point calculation rounding issues. If your order total is RS 150,000, multiplying by 100 converts it to 15000000 paisas so Stripe reads and captures the exact monetary value.

  • SDK Integration Parameters (ChargeCreateOptions):

    • Currency = "PKR" maps the transaction payload directly to your local currency.

    • Source = model.StripeToken accepts the single-use frontend security token generated by JavaScript back in Part 15, Step 3. This ensures that sensitive credit card details never touch your server logs.

  • Gateway Charge Validation Check:

    if (charge.Status == "succeeded")

    Once ChargeService.CreateAsync() communicates with the server networks, you evaluate the response string payload. If it matches "succeeded", the transaction is officially captured, and the code launches your post-purchase cleanup chain.

  • Post-Payment Cleanup and Notification Sequence:

    1. ProcessPaymentAsync: Updates the database order payment column status to Paid and maps Stripe's unique tracking receipt key (charge.Id) to the order row.

    2. ClearCartAsync: Disposes of all items inside the active shopping cart layout so the customer returns to an empty basket status.

    3. SendOrderConfirmationAsync: Dispatches your newly compiled HTML transactional receipt email built back in Step 4.

  • Targeted Exception Filters (StripeException): Wrapping the network charge routine in a distinct catch (StripeException ex) block separates gateway API errors (like expired cards or insufficient funds) from general server application errors. This enables your system to safely record the failure details via _logger while cleanly displaying the explicit user-facing error message using TempData.

Step 9: Implementing the Order Submission and Payment Branching Engine

This method orchestrates the absolute lifecycle of an order from submission to database lock down, handling defensive checks, input failures, and multi-channel fulfillment models.

C# / Controllers/CheckoutController.cs (Process Action)
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Process(CheckoutViewModel model)
{
    var cart = await _cartService.GetCartAsync();
    if (cart.CartItems.Count == 0)
    {
        TempData["Error"] = "Your cart is empty.";
        return RedirectToAction("Index", "ShoppingCart");
    }

    model.Cart = cart;

    if (!ModelState.IsValid)
    {
        ViewBag.CartItemCount = cart.ItemCount;
        ViewBag.StripePublishableKey = _configuration["Stripe:PublishableKey"];
        return View("Index", model);
    }

    try
    {
        var user = await _userManager.GetUserAsync(User);
        var userId = user?.Id;

        // Create order
        var order = await _orderService.CreateOrderAsync(model, userId);

        // Process payment based on method
        if (model.PaymentMethod == MobileShop.Models.PaymentMethod.Stripe)
        {
            return await ProcessStripePayment(order, model);
        }
        else if (model.PaymentMethod == MobileShop.Models.PaymentMethod.CashOnDelivery)
        {
            order.PaymentStatus = PaymentStatus.Pending;
            await _orderService.UpdateOrderStatusAsync(order.Id, OrderStatus.Pending);
        }
        else
        {
            // For other payment methods, mark as paid for demo
            await _orderService.ProcessPaymentAsync(order.Id, $"DEMO-{Guid.NewGuid()}");
        }

        // Clear cart
        await _cartService.ClearCartAsync();

        // Send confirmation email
        await _emailService.SendOrderConfirmationAsync(model.Email, order.OrderNumber, order.TotalAmount);

        return RedirectToAction("Confirmation", new { orderNumber = order.OrderNumber });
    }
    catch (Exception ex)
    {
        _logger.LogError($"Checkout error: {ex.Message}");
        ModelState.AddModelError("", "An error occurred during checkout. Please try again.");
        ViewBag.CartItemCount = cart.ItemCount;
        return View("Index", model);
    }
}

Core Logic & Architectural Breakdown

  • Defensive Boundary Security Guards:

    • [HttpPost] limits the endpoint to handle only network form submissions.

    • [ValidateAntiForgeryToken] intercepts the request to verify that the form data originates securely from an authentic session generated by your app, preventing malicious Cross-Site Request Forgery (CSRF) attacks.

    • The method runs another empty cart guard check just in case a user attempts a double-submit or uses duplicate browser tabs.

  • Re-hydrating UI Fallbacks on Invalid State (!ModelState.IsValid): If a required field (like a missing shipping phone number) fails verification, the engine drops out of execution. Crucially, because you are returning the user back to the "Index" view container, you must re-hydrate the layout dependencies by re-assigning ViewBag.CartItemCount and ViewBag.StripePublishableKey. Failing to do this will cause your layout headers or JavaScript elements to throw null-pointer errors upon page reload.

  • Unified Record Generation Layer (CreateOrderAsync): Before checking how the customer wants to pay, the system proactively generates the core database rows via _orderService.CreateOrderAsync(). This ensures that even if an online card processor experiences network dropouts, a tracked order entity has already been compiled and stored in your backend database system.

  • The Multi-Channel Payment Branching Architecture: The system evaluates the user's payment selection using a conditional control flow branch:

  • Payment Selection RouteCore Architectural Behavior
    StripeHands over entire thread execution directly to your private ProcessStripePayment helper method to run a real-time card capture check.
    Cash on Delivery (COD)Sets the invoice tracking state straight to PaymentStatus.Pending and registers the fulfillment workflow status to OrderStatus.Pending.
    Demo Gateways / OthersAutomatically marks the purchase invoice as immediately paid by appending a randomized tracking signature (DEMO-GUID), bypassing bank calls for development purposes.

Step 10: Creating the OrderConfirmationViewModel

By placing this file inside your ViewModels folder, you ensure that your Razor view receives exactly the data it needs to build the receipt page—nothing more, nothing less.

C# / ViewModels/OrderConfirmationViewModel.cs
using System.Collections.Generic;
using MobileShop.Models;

namespace MobileShop.ViewModels
{
    public class OrderConfirmationViewModel
    {
        public Order Order { get; set; } = null!;
        public List<OrderItem> OrderItems { get; set; } = new List<OrderItem>();
    }
}

Core C# Mechanics & Architectural Breakdown

  • The Safe Null Forgiving Operator (= null!;): By declaring public Order Order { get; set; } = null!;, you are using C# Nullable Reference Types (NRT) formatting. This safely tells the compiler: "I guarantee that this property will be explicitly populated by the controller before it is ever sent to the view." This suppresses annoying compiler nullability warnings while keeping your code clean.

  • Eager Collection Initialization (= new List<OrderItem>();): Instantiating the OrderItems property with an empty list by default is a defensive programming best practice. It completely eliminates the risk of throwing a frustrating NullReferenceException in your view if an order somehow goes through without any items attached.

  • Optimizing Data Flow & Preventing Lazy Loading Errors: In ASP.NET Core with Entity Framework, passing a raw database Order object straight to a frontend view can cause severe "Lazy Loading" crashes when the HTML loop tries to read related data (like Product.Name) after the database context has already been disposed of. This ViewModel acts as a secure memory container, holding all fully evaluated data ready for display.

Step 11: Implementing the Order Confirmation Landing Action

This method handles reading the freshly created order using its public order string, validating its existence, and setting up a dedicated confirmation model context.

C# / Controllers/CheckoutController.cs (Confirmation Action)
public async Task<IActionResult> Confirmation(string orderNumber)
{
    var order = await _orderService.GetOrderByNumberAsync(orderNumber);
    if (order == null)
    {
        return NotFound();
    }

    var viewModel = new OrderConfirmationViewModel
    {
        Order = order,
        OrderItems = order.OrderItems.ToList()
    };

    ViewBag.CartItemCount = 0;
    return View(viewModel);
}

Core Logic & Architectural Breakdown

  • Defensive Route Validation Check:

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

    This is a critical security and user-experience guard rail. If a user manually alters the URL query string or typing errors occur when tracking an order number, the system gracefully handles the empty result by returning a standard HTTP 404 NotFound status page, instead of throwing a null-pointer error on the screen.

  • Strict View Model Encapsulation: Instead of passing the raw domain database entity directly into the frontend layout, the code wraps the data inside an OrderConfirmationViewModel. Explicitly pulling down the related list collections using .ToList() ensures that all order snapshot information is fully loaded in memory before the view engine attempts compilation, completely bypassing any lazy-loading entity framework exceptions.

  • Explicit Header Interface Reset:

    ViewBag.CartItemCount = 0;

    Since the shopping cart state was completely cleared inside the previous Process method, this assignment guarantees that your shared website layout header instantly updates to display a count of 0 items in the navigation badge. This provides the shopper with clear visual confirmation that their purchase has been processed and their basket is empty.

Step 12: Breaking Down the Razor Order Confirmation View Layout

This view binds tightly to your strongly typed model data and converts financial parameters into a visual invoice block.

Razor / Views/Checkout/Confirmation.cshtml
@model OrderConfirmationViewModel
@{
    ViewData["Title"] = "Order Confirmation";
}

<div class="container py-5">
    <div class="text-center mb-5">
        <i class="bi bi-check-circle-fill text-success display-1"></i>
        <h2 class="mt-3">Thank You for Your Order!</h2>
        <p class="lead">Your order has been placed successfully.</p>
    </div>

    <div class="row justify-content-center">
        <div class="col-lg-8">
            <div class="card shadow">
                <div class="card-header bg-success text-white">
                    <h5 class="mb-0"><i class="bi bi-receipt"></i bind-id> Order Details</h5>
                </div>
                <div class="card-body">
                    <div class="row mb-4">
                        <div class="col-md-6">
                            <h6>Order Information</h6>
                            <p class="mb-1"><strong>Order Number:</strong> @Model.Order.OrderNumber</p>
                            <p class="mb-1"><strong>Order Date:</strong> @Model.Order.OrderDate.ToString("MMM dd, yyyy HH:mm")</p>
                            <p class="mb-1"><strong>Status:</strong> <span class="badge bg-warning">@Model.Order.Status</span></p>
                            <p class="mb-0"><strong>Payment Method:</strong> @Model.Order.PaymentMethod</p>
                        </div>
                        <div class="col-md-6">
                            <h6>Shipping Address</h6>
                            <p class="mb-1">@Model.Order.ShippingAddress</p>
                            <p class="mb-1">@Model.Order.ShippingCity, @Model.Order.ShippingPostalCode</p>
                            <p class="mb-0">@Model.Order.ShippingCountry</p>
                            @if (!string.IsNullOrEmpty(Model.Order.ShippingPhone))
                            {
                                <p class="mb-0"><strong>Phone:</strong> @Model.Order.ShippingPhone</p>
                            }
                        </div>
                    </div>

                    <h6>Order Items</h6>
                    <div class="table-responsive">
                        <table class="table">
                            <thead>
                                <tr>
                                    <th>Product</th>
                                    <th class="text-center">Quantity</th>
                                    <th class="text-end">Price</th>
                                    <th class="text-end">Total</th>
                                </tr>
                            </thead>
                            <tbody>
                                @foreach (var item in Model.OrderItems)
                                {
                                    <tr>
                                        <td>@item.Product?.Name</td>
                                        <td class="text-center">@item.Quantity</td>
                                        <td class="text-end">RS @item.UnitPrice.ToString("N0")</td bind-id>
                                        <td class="text-end">RS @item.TotalPrice.ToString("N0")</td>
                                    </tr>
                                }
                            </tbody>
                            <tfoot>
                                <tr>
                                    <td colspan="3" class="text-end"><strong>Subtotal:</strong></td>
                                    <td class="text-end">RS @Model.Order.Subtotal.ToString("N0")</td>
                                </tr>
                                <tr>
                                    <td colspan="3" class="text-end"><strong>Tax:</strong></td>
                                    <td class="text-end">RS @Model.Order.TaxAmount.ToString("N0")</td>
                                </tr>
                                <tr>
                                    <td colspan="3" class="text-end"><strong>Shipping:</strong></td>
                                    <td class="text-end">RS @Model.Order.ShippingCost.ToString("N0")</td>
                                </tr>
                                <tr class="table-primary">
                                    <td colspan="3" class="text-end"><h5 class="mb-0">Total:</h5></td>
                                    <td class="text-end"><h5 class="mb-0">RS @Model.Order.TotalAmount.ToString("N0")</h5></td>
                                </tr>
                            </tfoot>
                        </table>
                    </div>

                    @if (!string.IsNullOrEmpty(Model.Order.Notes))
                    {
                        <div class="alert alert-info mt-3">
                            <strong>Order Notes:</strong> @Model.Order.Notes
                        </div>
                    }
                </div>
            </div>

            <div class="text-center mt-4">
                <a asp-controller="Home" asp-action="Index" class="btn btn-primary btn-lg">
                    <i class="bi bi-shop"></i> Continue Shopping
                </a>
                <a asp-controller="Checkout" asp-action="TrackOrder" asp-route-orderNumber="@Model.Order.OrderNumber" class="btn btn-outline-primary btn-lg">
                    <i class="bi bi-truck"></i> Track Order
                </a>
            </div>
        </div>
    </div>
</div>

Core UI and Data Layer Breakdown

  • Strongly Typed Data Context Definition:

    @model OrderConfirmationViewModel

    By explicitly defining the view's model mapping token at the first line, Razor gains access to compile-time checking. This prevents text interpolation typos across your nested order properties during server rendering.

  • Split Receipt Matrix Grid (row, col-md-6): The template uses a 50/50 block grid split for medium screens and above. The left side handles core transaction metadata (like the unique generated order sequence and payment routing definitions). The right side isolates shipping coordinates, adapting cleanly when shrinking down onto mobile screen contexts.

  • Defensive Optional Rendering Block:

    @if (!string.IsNullOrEmpty(Model.Order.ShippingPhone)) { ... }

    Not all users will supply secondary delivery properties or custom purchase notes. By enclosing these fields in quick Razor logic checking blocks, you prevent empty text labels or broken spacing layers from appearing on the generated layout sheet.

  • Itemized Table Mapping Loop (@foreach): The view loops over the structural OrderItems property array list, generating standard rows dynamically. Notice how it calls .ToString("N0") alongside your local RS currency tag prefix. This handles standard grouping separators (e.g., displaying RS 150,000 instead of a raw unformatted 150000 literal string), providing a polished appearance.

  • The Clean UI State Recovery Router: At the very base of the component container, you provide two distinct navigation link components:

    • A clear direct route link button mapping back to the HomeController Index view so they can continue loading item lists.

    • An automated tracking loop route link passing the dynamic string code token (asp-route-orderNumber="@Model.Order.OrderNumber") ahead to prepare for your upcoming package tracking feature build.

Comments