Payment Module in ECommerce Application

How to Design and Develop Payment Module in ECommerce Application

In this article, I will discuss How to Design and Develop the Payment Module for our ECommerce Application using ASP.NET Core Web API and EF Core. Please read our previous article discussing the Order Module in our ECommerce Application.

The Payment Module is a critical component of the E-Commerce Application and manages all aspects of customer payments. This module ensures secure and efficient processing of transactions by integrating with reliable payment gateways, maintaining comprehensive payment records, and linking payments to corresponding orders. 

Key Features of the Payment Module

The following are the Key Features of the Payment Module we will implement in our Ecommerce application.

  • Payment Processing: Facilitates the initiation and handling of customer payments during the order placement process. it supports various payment methods such as Credit Cards, Cash on Delivery (COD), and PayPal. Ensures all payment data is handled securely, adhering to industry standards like PCI DSS. Connects with external payment gateways to process transactions reliably.
  • Payment Status Management: Tracks and updates the status of payments throughout their lifecycle. Monitors payment statuses such as Pending, Completed, Failed, etc. Automatically updates order statuses based on payment outcomes. Allows administrators to update payment statuses when necessary manually.
  • Payment Record Management: Maintains detailed records of all payment transactions. Captures essential information such as Payment ID, Order ID, Payment Method, Transaction ID, Amount, Payment Date, and Status. It stores historical payment data for reporting and auditing purposes.
  • Integration with Order Module: Automatically updates order statuses based on payment outcomes (e.g., from Pending to Processing). Can be integrated with notification systems to inform customers about payment confirmations and order updates.
Data Transfer Objects (DTOs):

DTOs (Data Transfer Objects) are pivotal in structuring and validating the data exchanged between the client and server. They ensure that only necessary and validated data is processed, enhancing security and maintaining a clear separation between internal data models and external API contracts. First, create a folder named PaymentDTOs within the DTOs folder where we will create all the Required Request and Response DTOs for managing the Payments.

PaymentRequestDTO

Create a class file named PaymentRequestDTO.cs within the DTOs\PaymentDTOs folder, and then copy and paste the following code. The PaymentRequestDTO Captures the necessary information to initiate a payment, including order ID, payment method, and amount. It is used when a customer proceeds to make a payment for an order, ensuring that all required payment details are provided and validated before processing.

using System.ComponentModel.DataAnnotations;

namespace ECommerceApp.DTOs.PaymentDTOs
{
    // DTO for initiating a payment
    public class PaymentRequestDTO
    {
        [Required(ErrorMessage = "CustomerId is required.")]
        public int CustomerId { get; set; }

        [Required(ErrorMessage = "Order ID is required.")]
        public int OrderId { get; set; }

        [Required(ErrorMessage = "Payment Method is required.")]
        [StringLength(50)]
        public string PaymentMethod { get; set; } // e.g., "CreditCard", "DebitCard", "PayPal", "COD"

        [Required(ErrorMessage = "Amount is required.")]
        [Range(0.01, double.MaxValue, ErrorMessage = "Amount must be greater than zero.")]
        public decimal Amount { get; set; }
    }
}
PaymentStatusUpdateDTO

Create a class file named PaymentStatusUpdateDTO.cs within the DTOs\PaymentDTOs folder, then copy and paste the following code. The PaymentStatusUpdateDTO Facilitates updating an existing payment’s status by capturing the payment ID and the new status. Administrators use it to update the status of payments based on processing outcomes, such as marking a payment as “Completed” or “Failed.”

using ECommerceApp.Models;
using System.ComponentModel.DataAnnotations;

namespace ECommerceApp.DTOs.PaymentDTOs
{
    // DTO for updating payment status
    public class PaymentStatusUpdateDTO
    {
        [Required(ErrorMessage = "Payment ID is required.")]
        public int PaymentId { get; set; }

        public string? TransactionId { get; set; }

        [Required(ErrorMessage = "Status is required.")]
        public PaymentStatus Status { get; set; } // e.g., "Completed", "Failed"
    }
}
PaymentResponseDTO

Create a class file named PaymentResponseDTO.cs within the DTOs\PaymentDTOs folder, and then copy and paste the following code. The PaymentResponseDTO Returns detailed payment information after processing, including identifiers, method, transaction details, amount, date, and status. It provides clients and administrators with comprehensive payment details after processing a payment.

using ECommerceApp.Models;

namespace ECommerceApp.DTOs.PaymentDTOs
{
    // DTO for payment response
    public class PaymentResponseDTO
    {
        public int PaymentId { get; set; }
        public int OrderId { get; set; }
        public string PaymentMethod { get; set; }
        public string? TransactionId { get; set; }
        public decimal Amount { get; set; }
        public DateTime PaymentDate { get; set; }
        public PaymentStatus Status { get; set; }
    }
}
CODPaymentUpdateDTO

Create a class file named CODPaymentUpdateDTO.cs within the DTOs\PaymentDTOs folder, then copy and paste the following code. This DTO marks the payment as accepted whenever the customer orders using the Cash On Delivery option. This DTO will be used when the order is Delivered and Payment is received.

using System.ComponentModel.DataAnnotations;
namespace ECommerceApp.DTOs.PaymentDTOs
{
    public class CODPaymentUpdateDTO
    {
        [Required(ErrorMessage = "Order ID is required.")]
        public int OrderId { get; set; }

        [Required(ErrorMessage = "Payment Id is required.")]
        public int PaymentId { get; set; }
    }
}
Updating PaymentStatus Enum Values:

Look at the Statuses Master table in the database, and you will see the following data.

Updating PaymentStatus Enum Values

Now, we want to ensure the ID, whatever is stored in the database for each payment status, is the same as our PaymentStatus enum named constants. So, please modify the PaymentStatus enum to sync to values with the database. This is important to ensure the Payment Status is reflected correctly in the database.

using System.Text.Json.Serialization;
namespace ECommerceApp.Models
{
    [JsonConverter(typeof(JsonStringEnumConverter))]
    public enum PaymentStatus
    {
        Pending = 1,
        Completed = 6,
        Failed = 7,
        Refunded = 10
    }
}
Configuring Email Service in our Application:

An email service in the context of web applications is a mechanism that allows the application to send emails to users programmatically. An Email Service is a service responsible for managing email communication within an application. This can include sending notifications, confirmations, password reset links, verification emails, newsletters to users, promotional materials, and more.

In web development, email services are typically implemented through an SMTP (Simple Mail Transfer Protocol) server that handles sending these emails from your application. Email Service often supports different providers (Gmail, Outlook, SendGrid, AWS SES, etc.). Here, we will use the Gmail SMTP server to manage the Emails.

How to Configure Gmail SMTP Settings?

To send emails through Gmail’s SMTP server, we need to configure several settings:

  • SMTP Server Address: The SMTP server for Gmail is gmail.com.
  • Port: Use SSL (Secure Socket Layer) on port 465. If you’re using TLS (Transport Layer Security), use port 587
  • Authentication: Gmail requires authentication. Also, please make sure 2FA is enabled for your account.
  • Username: Your Gmail email address (e.g., your-email@gmail.com).
  • Password: Your Gmail account password will not work, and you must create an App Password.
Set Up SMTP Configuration in AppSettings.Json File:

Integrating Gmail’s SMTP with an ASP.NET Core application involves setting up the necessary configurations and implementing email-sending functionality. Let us proceed and understand how to integrate Gmail’s SMTP with an ASP.NET Core application step by step. Add the SMTP settings to the appsettings.json file. So, please modify the appsettings.json file as follows:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "EFCoreDBConnection": "Server=LAPTOP-6P5NK25R\\SQLSERVER2022DEV;Database=ECommerceDB;Trusted_Connection=True;TrustServerCertificate=True;"
  },
  "EmailSettings": {
    "MailServer": "smtp.gmail.com",
    "MailPort": 587,
    "SenderName": "Your Application Name",
    "FromEmail": "yourgmail@gmail.com",
    "Password": "yourAppPassword"
  }
}

Replace yourgmail@gmail.com and yourAppPassword with your actual Gmail ID and app password. Also, give a meaningful sender name.

Create the EmailService

Create a new class file named EmailService.cs within the Services folder, and then copy and paste the following code. The following code is self-explained, so please read the comment lines for a better understanding.

using System.Net.Mail;
using System.Net;

namespace ECommerceApp.Services
{
    public class EmailService
    {
        // Configuration property to access application settings.
        private readonly IConfiguration _configuration;

        // Constructor that injects the configuration dependency.
        public EmailService(IConfiguration configuration)
        {
            // Save the configuration object for later use.
            _configuration = configuration;
        }

        // Method to send an email asynchronously.
        public Task SendEmailAsync(string ToEmail, string Subject, string Body, bool IsBodyHtml = false)
        {
            // Retrieve the mail server (SMTP host) from the configuration.
            string? MailServer = _configuration["EmailSettings:MailServer"];

            // Retrieve the sender email address from the configuration.
            string? FromEmail = _configuration["EmailSettings:FromEmail"];

            // Retrieve the sender email password from the configuration.
            string? Password = _configuration["EmailSettings:Password"];

            // Retrieve the sender's display name from the configuration.
            string? SenderName = _configuration["EmailSettings:SenderName"];

            // Retrieve the SMTP port number from the configuration and convert it to an integer.
            int Port = Convert.ToInt32(_configuration["EmailSettings:MailPort"]);

            // Create a new instance of SmtpClient using the mail server and port number.
            var client = new SmtpClient(MailServer, Port)
            {
                // Set the credentials (email and password) for the SMTP server.
                Credentials = new NetworkCredential(FromEmail, Password),

                // Enable SSL for secure email communication.
                EnableSsl = true,
            };

            // Create a MailAddress object with the sender's email and display name.
            MailAddress fromAddress = new MailAddress(FromEmail, SenderName);

            // Create a new MailMessage object to define the email's properties.
            MailMessage mailMessage = new MailMessage
            {
                From = fromAddress, // Set the sender's email address with display name.
                Subject = Subject, // Set the email subject line.
                Body = Body, // Set the email body content.
                IsBodyHtml = IsBodyHtml // Specify whether the body content is in HTML format.
            };

            // Add the recipient's email address to the message.
            mailMessage.To.Add(ToEmail);

            // Send the email asynchronously using the SmtpClient instance.
            return client.SendMailAsync(mailMessage);
        }
    }
}
Create the Payment Service Class

The PaymentService encapsulates all business and data access operations related to payment processing. Its responsibilities include validating and processing payments, retrieving payment details, updating payment statuses, and handling Cash on Delivery (COD) completion. So, create a class file named PaymentService.cs within the Services folder and then copy and paste the following code:

using ECommerceApp.Data;
using ECommerceApp.DTOs;
using ECommerceApp.DTOs.PaymentDTOs;
using ECommerceApp.Models;
using Microsoft.EntityFrameworkCore;

namespace ECommerceApp.Services
{
    public class PaymentService
    {
        private readonly ApplicationDbContext _context;
        private readonly EmailService _emailService;

        public PaymentService(ApplicationDbContext context, EmailService emailService)
        {
            _context = context;
            _emailService = emailService;
        }

        public async Task<ApiResponse<PaymentResponseDTO>> ProcessPaymentAsync(PaymentRequestDTO paymentRequest)
        {
            // Use a transaction to guarantee atomic operations on Order and Payment
            using (var transaction = await _context.Database.BeginTransactionAsync())
            {
                try
                {
                    // Retrieve the order along with any existing payment record
                    var order = await _context.Orders
                        .Include(o => o.Payment)
                        .FirstOrDefaultAsync(o => o.Id == paymentRequest.OrderId && o.CustomerId == paymentRequest.CustomerId);

                    if (order == null)
                    {
                        return new ApiResponse<PaymentResponseDTO>(404, "Order not found.");
                    }

                    if (Math.Round(paymentRequest.Amount, 2) != Math.Round(order.TotalAmount, 2))
                    {
                        return new ApiResponse<PaymentResponseDTO>(400, "Payment amount does not match the order total.");
                    }

                    Payment payment;

                    // Check if an existing payment record is present
                    if (order.Payment != null)
                    {
                        // Allow retry only if previous payment failed and order status is still Pending
                        if (order.Payment.Status == PaymentStatus.Failed && order.OrderStatus == OrderStatus.Pending)
                        {
                            // Retry: update the existing payment record with new details
                            payment = order.Payment;

                            payment.PaymentMethod = paymentRequest.PaymentMethod;
                            payment.Amount = paymentRequest.Amount;
                            payment.PaymentDate = DateTime.UtcNow;
                            payment.Status = PaymentStatus.Pending;
                            payment.TransactionId = null; // Clear previous transaction id if any
                            _context.Payments.Update(payment);
                        }
                        else
                        {
                            return new ApiResponse<PaymentResponseDTO>(400, "Order already has an associated payment.");
                        }
                    }
                    else
                    {
                        // Create a new Payment record if none exists
                        payment = new Payment
                        {
                            OrderId = paymentRequest.OrderId,
                            PaymentMethod = paymentRequest.PaymentMethod,
                            Amount = paymentRequest.Amount,
                            PaymentDate = DateTime.UtcNow,
                            Status = PaymentStatus.Pending
                        };

                        _context.Payments.Add(payment);
                    }

                    // For non-COD payments, simulate payment processing
                    if (!IsCashOnDelivery(paymentRequest.PaymentMethod))
                    {
                        var simulatedStatus = await SimulatePaymentGateway();
                        payment.Status = simulatedStatus;

                        if (simulatedStatus == PaymentStatus.Completed)
                        {
                            // Update the Transaction Id on successful payment
                            payment.TransactionId = GenerateTransactionId();

                            // Update order status accordingly
                            order.OrderStatus = OrderStatus.Processing;
                        }
                    }
                    else
                    {
                        // For COD, mark the order status as Processing immediately
                        order.OrderStatus = OrderStatus.Processing;
                    }

                    await _context.SaveChangesAsync();
                    await transaction.CommitAsync();

                    // Send Order Confirmation Email if Order Status is Processing
                    // It means the user is either selected COD of the Payment is Sucessful 
                    if (order.OrderStatus == OrderStatus.Processing)
                    {
                        await SendOrderConfirmationEmailAsync(paymentRequest.OrderId);
                    }

                    // Manual mapping to PaymentResponseDTO
                    var paymentResponse = new PaymentResponseDTO
                    {
                        PaymentId = payment.Id,
                        OrderId = payment.OrderId,
                        PaymentMethod = payment.PaymentMethod,
                        TransactionId = payment.TransactionId,
                        Amount = payment.Amount,
                        PaymentDate = payment.PaymentDate,
                        Status = payment.Status
                    };

                    return new ApiResponse<PaymentResponseDTO>(200, paymentResponse);
                }
                catch (Exception)
                {
                    await transaction.RollbackAsync();
                    return new ApiResponse<PaymentResponseDTO>(500, "An unexpected error occurred while processing the payment.");
                }
            }
        }

        public async Task<ApiResponse<PaymentResponseDTO>> GetPaymentByIdAsync(int paymentId)
        {
            try
            {
                var payment = await _context.Payments
                    .AsNoTracking()
                    .FirstOrDefaultAsync(p => p.Id == paymentId);

                if (payment == null)
                {
                    return new ApiResponse<PaymentResponseDTO>(404, "Payment not found.");
                }

                var paymentResponse = new PaymentResponseDTO
                {
                    PaymentId = payment.Id,
                    OrderId = payment.OrderId,
                    PaymentMethod = payment.PaymentMethod,
                    TransactionId = payment.TransactionId,
                    Amount = payment.Amount,
                    PaymentDate = payment.PaymentDate,
                    Status = payment.Status
                };

                return new ApiResponse<PaymentResponseDTO>(200, paymentResponse);
            }
            catch (Exception)
            {
                return new ApiResponse<PaymentResponseDTO>(500, "An unexpected error occurred while retrieving the payment.");
            }
        }

        public async Task<ApiResponse<PaymentResponseDTO>> GetPaymentByOrderIdAsync(int orderId)
        {
            try
            {
                var payment = await _context.Payments
                    .AsNoTracking()
                    .FirstOrDefaultAsync(p => p.OrderId == orderId);

                if (payment == null)
                {
                    return new ApiResponse<PaymentResponseDTO>(404, "Payment not found for this order.");
                }

                var paymentResponse = new PaymentResponseDTO
                {
                    PaymentId = payment.Id,
                    OrderId = payment.OrderId,
                    PaymentMethod = payment.PaymentMethod,
                    TransactionId = payment.TransactionId,
                    Amount = payment.Amount,
                    PaymentDate = payment.PaymentDate,
                    Status = payment.Status
                };

                return new ApiResponse<PaymentResponseDTO>(200, paymentResponse);
            }
            catch (Exception)
            {
                return new ApiResponse<PaymentResponseDTO>(500, "An unexpected error occurred while retrieving the payment.");
            }
        }

        public async Task<ApiResponse<ConfirmationResponseDTO>> UpdatePaymentStatusAsync(PaymentStatusUpdateDTO statusUpdate)
        {
            try
            {
                var payment = await _context.Payments
                    .Include(p => p.Order)
                    .FirstOrDefaultAsync(p => p.Id == statusUpdate.PaymentId);

                if (payment == null)
                {
                    return new ApiResponse<ConfirmationResponseDTO>(404, "Payment not found.");
                }
                
                payment.Status = statusUpdate.Status;

                // Update order status if payment is now completed and the method is not COD
                if (statusUpdate.Status == PaymentStatus.Completed && !IsCashOnDelivery(payment.PaymentMethod))
                {
                    payment.TransactionId = statusUpdate.TransactionId;
                    payment.Order.OrderStatus = OrderStatus.Processing;
                }

                await _context.SaveChangesAsync();

                // Send Order Confirmation Email if Order Status is Processing
                if (payment.Order.OrderStatus == OrderStatus.Processing)
                {
                    await SendOrderConfirmationEmailAsync(payment.Order.Id);
                }

                var confirmation = new ConfirmationResponseDTO
                {
                    Message = $"Payment with ID {payment.Id} updated to status '{payment.Status}'."
                };

                return new ApiResponse<ConfirmationResponseDTO>(200, confirmation);
            }
            catch (Exception)
            {
                return new ApiResponse<ConfirmationResponseDTO>(500, "An unexpected error occurred while updating the payment status.");
            }
        }

        public async Task<ApiResponse<ConfirmationResponseDTO>> CompleteCODPaymentAsync(CODPaymentUpdateDTO codPaymentUpdateDTO)
        {
            using (var transaction = await _context.Database.BeginTransactionAsync())
            {
                try
                {
                    var payment = await _context.Payments
                    .Include(p => p.Order)
                    .FirstOrDefaultAsync(p => p.Id == codPaymentUpdateDTO.PaymentId && p.OrderId == codPaymentUpdateDTO.OrderId);

                    if (payment == null)
                    {
                        return new ApiResponse<ConfirmationResponseDTO>(404, "Payment not found.");
                    }

                    if (payment.Order == null)
                    {
                        return new ApiResponse<ConfirmationResponseDTO>(404, "No Order associated with this Payment.");
                    }

                    if (payment.Order.OrderStatus != OrderStatus.Shipped)
                    {
                        return new ApiResponse<ConfirmationResponseDTO>(400, $"Order cannot be marked as Delivered from {payment.Order.OrderStatus} State");
                    }

                    if (!IsCashOnDelivery(payment.PaymentMethod))
                    {
                        return new ApiResponse<ConfirmationResponseDTO>(409, "Payment method is not Cash on Delivery.");
                    }

                    payment.Status = PaymentStatus.Completed;
                    payment.Order.OrderStatus = OrderStatus.Delivered;

                    await _context.SaveChangesAsync();
                    await transaction.CommitAsync();

                    var confirmation = new ConfirmationResponseDTO
                    {
                        Message = $"COD Payment for Order ID {payment.Order.Id} has been marked as 'Completed' and the order status updated to 'Delivered'."
                    };

                    return new ApiResponse<ConfirmationResponseDTO>(200, confirmation);
                }
                catch (Exception)
                {
                    await transaction.RollbackAsync();
                    return new ApiResponse<ConfirmationResponseDTO>(500, "An unexpected error occurred while completing the COD payment.");
                }
            }
        }

        #region Helper Methods

        // Simulate a payment gateway response using Random.Shared for performance
        private async Task<PaymentStatus> SimulatePaymentGateway()
        {
            //Simulate the PG
            await Task.Delay(TimeSpan.FromMilliseconds(1));

            int chance = Random.Shared.Next(1, 101); // 1 to 100

            if (chance <= 60)
                return PaymentStatus.Completed;
            else if (chance <= 90)
                return PaymentStatus.Pending;
            else
                return PaymentStatus.Failed;
        }

        // Generate a unique 12-character transaction ID
        private string GenerateTransactionId()
        {
            return $"TXN-{Guid.NewGuid().ToString("N").ToUpper().Substring(0, 12)}";
        }

        // Determines if the provided payment method indicates Cash on Delivery
        private bool IsCashOnDelivery(string paymentMethod)
        {
            return paymentMethod.Equals("CashOnDelivery", StringComparison.OrdinalIgnoreCase) ||
                   paymentMethod.Equals("COD", StringComparison.OrdinalIgnoreCase);
        }

        // Fetches the complete order details (including discount, shipping cost, and summary),
        // builds a professional HTML email body, and sends it to the customer.
        public async Task SendOrderConfirmationEmailAsync(int orderId)
        {
            // Retrieve the order with its related customer, addresses, payment, and order items (with products)
            var order = await _context.Orders
                .AsNoTracking()
                .Include(o => o.Customer)
                .Include(o => o.BillingAddress)
                .Include(o => o.ShippingAddress)
                .Include(o => o.Payment)
                .Include(o => o.OrderItems)
                    .ThenInclude(oi => oi.Product)
                .FirstOrDefaultAsync(o => o.Id == orderId);

            if (order == null)
            {
                // Optionally log that the order was not found.
                return;
            }

            var payment = order.Payment; // Payment details may be null if not available

            // Prepare the email subject.
            string subject = $"Order Confirmation - {order.OrderNumber}";

            // Build the HTML email body using string interpolation.
            string emailBody = $@"
            <html>
              <head>
                <meta charset='UTF-8'>
              </head>
              <body style='font-family: Arial, sans-serif; background-color: #f4f4f4; margin: 0; padding: 20px;'>
                <div style='max-width: 700px; margin: auto; background-color: #ffffff; padding: 20px; border: 1px solid #dddddd;'>
                  <!-- Header -->
                  <div style='background-color: #007bff; padding: 15px; text-align: center; color: #ffffff;'>
                    <h2 style='margin: 0;'>Order Confirmation</h2>
                  </div>
          
                  <!-- Greeting and Order Details -->
                  <p style='margin: 20px 0 5px 0;'>Dear {order.Customer.FirstName} {order.Customer.LastName},</p>
                  <p style='margin: 5px 0 20px 0;'>Thank you for your order. Please find your invoice below.</p>
                  <table style='width: 100%; border-collapse: collapse; margin-bottom: 20px;'>
                    <tr>
                      <td style='padding: 8px; background-color: #f8f8f8; border: 1px solid #dddddd;'><strong>Order Number:</strong></td>
                      <td style='padding: 8px; border: 1px solid #dddddd;'>{order.OrderNumber}</td>
                    </tr>
                    <tr>
                      <td style='padding: 8px; background-color: #f8f8f8; border: 1px solid #dddddd;'><strong>Order Date:</strong></td>
                      <td style='padding: 8px; border: 1px solid #dddddd;'>{order.OrderDate:MMMM dd, yyyy}</td>
                    </tr>
                  </table>
          
                  <!-- Order Summary (placed before order items) -->
                  <h3 style='color: #007bff; border-bottom: 2px solid #eeeeee; padding-bottom: 5px;'>Order Summary</h3>
                  <table style='width: 100%; border-collapse: collapse; margin-bottom: 20px;'>
                    <tr>
                      <td style='padding: 8px; background-color: #f8f8f8; border: 1px solid #dddddd;'><strong>Sub Total:</strong></td>
                      <td style='padding: 8px; border: 1px solid #dddddd;'>{order.TotalBaseAmount:C}</td>
                    </tr>
                    <tr>
                      <td style='padding: 8px; background-color: #f8f8f8; border: 1px solid #dddddd;'><strong>Discount:</strong></td>
                      <td style='padding: 8px; border: 1px solid #dddddd;'>-{order.TotalDiscountAmount:C}</td>
                    </tr>
                    <tr>
                      <td style='padding: 8px; background-color: #f8f8f8; border: 1px solid #dddddd;'><strong>Shipping Cost:</strong></td>
                      <td style='padding: 8px; border: 1px solid #dddddd;'>{order.ShippingCost:C}</td>
                    </tr>
                    <tr style='font-weight: bold;'>
                      <td style='padding: 8px; background-color: #f8f8f8; border: 1px solid #dddddd;'><strong>Total Amount:</strong></td>
                      <td style='padding: 8px; border: 1px solid #dddddd;'>{order.TotalAmount:C}</td>
                    </tr>
                  </table>
          
                  <!-- Order Items -->
                  <h3 style='color: #007bff; border-bottom: 2px solid #eeeeee; padding-bottom: 5px;'>Order Items</h3>
                  <table style='width: 100%; border-collapse: collapse; margin-bottom: 20px;'>
                    <tr style='background-color: #28a745; color: #ffffff;'>
                      <th style='padding: 8px; border: 1px solid #dddddd;'>Product</th>
                      <th style='padding: 8px; border: 1px solid #dddddd;'>Quantity</th>
                      <th style='padding: 8px; border: 1px solid #dddddd;'>Unit Price</th>
                      <th style='padding: 8px; border: 1px solid #dddddd;'>Discount</th>
                      <th style='padding: 8px; border: 1px solid #dddddd;'>Total Price</th>
                    </tr>
                    {string.Join("", order.OrderItems.Select(item => $@"
                    <tr>
                      <td style='padding: 8px; border: 1px solid #dddddd;'>{item.Product.Name}</td>
                      <td style='padding: 8px; border: 1px solid #dddddd;'>{item.Quantity}</td>
                      <td style='padding: 8px; border: 1px solid #dddddd;'>{item.UnitPrice:C}</td>
                      <td style='padding: 8px; border: 1px solid #dddddd;'>{item.Discount:C}</td>
                      <td style='padding: 8px; border: 1px solid #dddddd;'>{item.TotalPrice:C}</td>
                    </tr>
                    "))}
                  </table>
          
                  <!-- Addresses: Combined Billing and Shipping -->
                  <h3 style='color: #007bff; border-bottom: 2px solid #eeeeee; padding-bottom: 5px;'>Addresses</h3>
                  <table style='width: 100%; border-collapse: collapse; margin-bottom: 20px;'>
                    <tr>
                      <td style='width: 50%; vertical-align: top; padding: 8px; border: 1px solid #dddddd;'>
                        <strong>Billing Address</strong><br/>
                        {order.BillingAddress.AddressLine1}<br/>
                        {(string.IsNullOrWhiteSpace(order.BillingAddress.AddressLine2) ? "" : order.BillingAddress.AddressLine2 + "<br/>")}
                        {order.BillingAddress.City}, {order.BillingAddress.State} {order.BillingAddress.PostalCode}<br/>
                        {order.BillingAddress.Country}
                      </td>
                      <td style='width: 50%; vertical-align: top; padding: 8px; border: 1px solid #dddddd;'>
                        <strong>Shipping Address</strong><br/>
                        {order.ShippingAddress.AddressLine1}<br/>
                        {(string.IsNullOrWhiteSpace(order.ShippingAddress.AddressLine2) ? "" : order.ShippingAddress.AddressLine2 + "<br/>")}
                        {order.ShippingAddress.City}, {order.ShippingAddress.State} {order.ShippingAddress.PostalCode}<br/>
                        {order.ShippingAddress.Country}
                      </td>
                    </tr>
                  </table>
          
                  <!-- Payment Details -->
                  <h3 style='color: #007bff; border-bottom: 2px solid #eeeeee; padding-bottom: 5px;'>Payment Details</h3>
                  <table style='width: 100%; border-collapse: collapse; margin-bottom: 20px;'>
                    <tr>
                      <td style='padding: 8px; background-color: #f8f8f8; border: 1px solid #dddddd;'><strong>Payment Method:</strong></td>
                      <td style='padding: 8px; border: 1px solid #dddddd;'>{(payment != null ? payment.PaymentMethod : "N/A")}</td>
                    </tr>
                    <tr>
                      <td style='padding: 8px; background-color: #f8f8f8; border: 1px solid #dddddd;'><strong>Payment Date:</strong></td>
                      <td style='padding: 8px; border: 1px solid #dddddd;'>{(payment != null ? payment.PaymentDate.ToString("MMMM dd, yyyy HH:mm") : "N/A")}</td>
                    </tr>
                    <tr>
                      <td style='padding: 8px; background-color: #f8f8f8; border: 1px solid #dddddd;'><strong>Transaction ID:</strong></td>
                      <td style='padding: 8px; border: 1px solid #dddddd;'>{(payment != null ? payment.TransactionId : "N/A")}</td>
                    </tr>
                    <tr>
                      <td style='padding: 8px; background-color: #f8f8f8; border: 1px solid #dddddd;'><strong>Status:</strong></td>
                      <td style='padding: 8px; border: 1px solid #dddddd;'>{(payment != null ? payment.Status.ToString() : "N/A")}</td>
                    </tr>
                  </table>
          
                  <p style='margin-top: 20px;'>If you have any questions, please contact our support team.</p>
                  <p>Best regards,<br/>Your E-Commerce Store Team</p>
                </div>
              </body>
            </html>";

            // Send the email using the EmailService.
            await _emailService.SendEmailAsync(order.Customer.Email, subject, emailBody, IsBodyHtml: true);
        }

        #endregion
    }
}
Registering Payment and Email Services in Dependency Injection Container

We need to register the services within the dependency injection container to enable dependency injection of the Payment and Email Services. So, please modify the Program.cs class file as follows:

using ECommerceApp.Data;
using ECommerceApp.Services;
using Microsoft.EntityFrameworkCore;

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

            // Add services to the container.

            builder.Services.AddControllers()
            .AddJsonOptions(options =>
            {
                // This will use the property names as defined in the C# model
                options.JsonSerializerOptions.PropertyNamingPolicy = null;
            });

            // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
            builder.Services.AddEndpointsApiExplorer();
            builder.Services.AddSwaggerGen();

            // Configure EF Core with SQL Server
            builder.Services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(builder.Configuration.GetConnectionString("EFCoreDBConnection")));

            // Registering the CustomerService
            builder.Services.AddScoped<CustomerService>();

            // Registering the AddressService
            builder.Services.AddScoped<AddressService>();

            // Registering the CategoryService
            builder.Services.AddScoped<CategoryService>();

            // Registering the ProductService
            builder.Services.AddScoped<ProductService>();

            // Registering the ShoppingCartService
            builder.Services.AddScoped<ShoppingCartService>();

            // Registering the OrderService
            builder.Services.AddScoped<OrderService>();

            // Registering the PaymentService
            builder.Services.AddScoped<PaymentService>();

            // Registering the EmailService
            builder.Services.AddScoped<EmailService>();

            var app = builder.Build();

            // Configure the HTTP request pipeline.
            if (app.Environment.IsDevelopment())
            {
                app.UseSwagger();
                app.UseSwaggerUI();
            }

            app.UseHttpsRedirection();

            app.UseAuthorization();

            app.MapControllers();

            app.Run();
        }
    }
}
Creating Payments Controller

The PaymentsController manages all payments-related operations, ensuring that payments are processed securely and efficiently. Each action method is designed to handle specific aspects of payment management, enforcing validation and maintaining data integrity throughout the process. Create a new API Empty Controller named PaymentsController within the Controllers folder and then copy and paste the following code.

using Microsoft.AspNetCore.Mvc;
using ECommerceApp.DTOs;
using ECommerceApp.DTOs.PaymentDTOs;
using ECommerceApp.Services;

namespace ECommerceApp.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class PaymentsController : ControllerBase
    {
        private readonly PaymentService _paymentService;

        public PaymentsController(PaymentService paymentService)
        {
            _paymentService = paymentService;
        }

        // Processes a payment for an order.
        [HttpPost("ProcessPayment")]
        public async Task<ActionResult<ApiResponse<PaymentResponseDTO>>> ProcessPayment([FromBody] PaymentRequestDTO paymentRequest)
        {
            var response = await _paymentService.ProcessPaymentAsync(paymentRequest);
            if (response.StatusCode != 200)
            {
                return StatusCode(response.StatusCode, response);
            }
            return Ok(response);
        }

        // Retrieves payment details by Payment ID.
        [HttpGet("GetPaymentById/{paymentId}")]
        public async Task<ActionResult<ApiResponse<PaymentResponseDTO>>> GetPaymentById(int paymentId)
        {
            var response = await _paymentService.GetPaymentByIdAsync(paymentId);
            if (response.StatusCode != 200)
            {
                return StatusCode(response.StatusCode, response);
            }
            return Ok(response);
        }

        // Retrieves payment details associated with a specific order.
        [HttpGet("GetPaymentByOrderId/{orderId}")]
        public async Task<ActionResult<ApiResponse<PaymentResponseDTO>>> GetPaymentByOrderId(int orderId)
        {
            var response = await _paymentService.GetPaymentByOrderIdAsync(orderId);
            if (response.StatusCode != 200)
            {
                return StatusCode(response.StatusCode, response);
            }
            return Ok(response);
        }

        // Updates the status of an existing payment.
        [HttpPut("UpdatePaymentStatus")]
        public async Task<ActionResult<ApiResponse<ConfirmationResponseDTO>>> UpdatePaymentStatus([FromBody] PaymentStatusUpdateDTO statusUpdate)
        {
            var response = await _paymentService.UpdatePaymentStatusAsync(statusUpdate);
            if (response.StatusCode != 200)
            {
                return StatusCode(response.StatusCode, response);
            }
            return Ok(response);
        }

        // Completes a Cash on Delivery (COD) payment.
        [HttpPost("CompleteCODPayment")]
        public async Task<ActionResult<ApiResponse<ConfirmationResponseDTO>>> CompleteCODPayment([FromBody] CODPaymentUpdateDTO codPaymentUpdateDTO)
        {
            var response = await _paymentService.CompleteCODPaymentAsync(codPaymentUpdateDTO);
            if (response.StatusCode != 200)
            {
                return StatusCode(response.StatusCode, response);
            }
            return Ok(response);
        }
    }
}
Testing the Payment Endpoints:

Let us test each payment-related endpoint of our Ecommerce Application. Please replace {port} with the port number your application is running on (e.g., 5000 or 443).

Process Payment:

This endpoint handles initiating and processing a new payment, ensuring that all necessary validations are performed and the payment is securely recorded. It is invoked when a customer submits a payment for an order, requiring the system to process the payment, update order statuses, and maintain accurate payment records. Provides clear feedback to customers and administrators regarding the outcome of payment processing.

Method: POST
URL: http://localhost:{port}/api/Payments/ProcessPayment
Headers: Content-Type: application/json
Body (JSON):

{
  "CustomerId": 1,
  "OrderId": 1,
  "PaymentMethod": "CreditCard",
  "Amount": 3819.96
}

Expected Response: A JSON object with the processed payment details (including PaymentId, TransactionId, etc.).

How to Design and Develop the Payment Module for our ECommerce Application using ASP.NET Core Web API and EF Core

Get Payment by ID:

Retrieves detailed information about a specific payment based on its unique identifier. It is used by administrators or customers to view comprehensive details of a particular payment, including method, transaction details, amount, date, and status.

Method: GET
URL: http://localhost:{port}/Payments/GetPaymentById/1
Expected Response: The payment details for the given payment ID.

How to Design and Develop the Payment Module for our ECommerce Application using ASP.NET Core Web API

Get Payment by Order ID:

Retrieves the payment details associated with a specific order. It is used by administrators or customers to view payment information linked to a particular order, aiding in order tracking and financial reconciliation.

Method: GET
URL: http://localhost:{port}//Payments/GetPaymentByOrderId/123
Expected Response: The payment details associated with the given order.

How to Design and Develop the Payment Module for our ECommerce Application

Update Payment Status:

Enables updating an existing payment’s status, ensuring that updates adhere to business rules and reflect accurate payment outcomes. It is used by administrators to manually update the status of payments, such as marking a payment as “Completed” after manual verification or adjusting statuses based on external factors.

Method: PUT
URL: http://localhost:{port}/api/Payments/UpdatePaymentStatus
Headers: Content-Type: application/json
Body (JSON):

{
  "PaymentId": 1,
  "TransactionId": "TXN-1234-XYZ",
  "Status": "Completed"
}

Expected Response: A confirmation message that the payment status has been updated.

Complete COD Payment:

Marks the payment as completed for Cash on Delivery (COD) payments upon order delivery, updating both payment and order statuses accordingly. It is used by administrators to confirm COD payments after the successful delivery of an order, ensuring that the payment and order records are accurately updated.

Method: POST
URL: http://localhost:{port}/api/Payments/CompleteCODPayment
Headers: Content-Type: application/json
Body (JSON):

{
  "PaymentId": 1,
  "OrderId": 1
}

Expected Response: A confirmation message that the COD payment has been marked as completed and the order status updated.

Implementing a Background Service for Pending Payments

To handle payments with a “Pending” status automatically, we will implement a Background Service that periodically checks and updates these payments based on actual PG responses. So, create a class file named PendingPaymentService.cs within the Services folder and then copy and paste the following code:

using ECommerceApp.Data;
using ECommerceApp.Models;
using Microsoft.EntityFrameworkCore;

namespace ECommerceApp.Services
{
    public class PendingPaymentService : BackgroundService
    {
        private readonly IServiceProvider _serviceProvider;
        private readonly TimeSpan _checkInterval = TimeSpan.FromMinutes(5); // Adjust as needed

        public PendingPaymentService(IServiceProvider serviceProvider)
        {
            _serviceProvider = serviceProvider;
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                try
                {
                    using (var scope = _serviceProvider.CreateScope())
                    {
                        var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();

                        // Retrieve payments with status "Pending"
                        var pendingPayments = await context.Payments
                            .Include(p => p.Order)
                            .Where(p => p.Status == PaymentStatus.Pending && p.PaymentMethod.ToUpper() != "COD")
                            .ToListAsync(stoppingToken);

                        // List to track orders for which we need to send confirmation emails.
                        var ordersToEmail = new List<int>();

                        foreach (var payment in pendingPayments)
                        {
                            // Simulate checking payment status
                            string updatedStatus = SimulatePaymentGatewayResponse();

                            if (updatedStatus == "Completed")
                            {
                                payment.Status = PaymentStatus.Completed;
                                payment.Order.OrderStatus = OrderStatus.Processing;
                                ordersToEmail.Add(payment.Order.Id);
                            }
                            else if (updatedStatus == "Failed")
                            {
                                payment.Status = PaymentStatus.Failed;
                            }
                            // If "Pending", no change

                            context.Payments.Update(payment);
                            context.Orders.Update(payment.Order);
                        }

                        // Save all status updates.
                        await context.SaveChangesAsync(stoppingToken);

                        // If there are any orders that have been updated to Processing, send order confirmation emails.
                        if (ordersToEmail.Any())
                        {
                            // Retrieve the PaymentService which has our email sending method.
                            var paymentService = scope.ServiceProvider.GetRequiredService<PaymentService>();

                            foreach (var orderId in ordersToEmail)
                            {
                                // Send the order confirmation email.
                                await paymentService.SendOrderConfirmationEmailAsync(orderId);
                            }
                        }
                    }
                }
                catch (Exception ex)
                {
                    // Log the exception as needed.
                }

                // Wait for the next interval.
                await Task.Delay(_checkInterval, stoppingToken);
            }
        }

        // Simulates a response from the payment gateway for pending payments.
        // Returns updated payment status: "Completed", "Failed", or "Pending".
        private string SimulatePaymentGatewayResponse()
        {
            // Simulate payment gateway response:
            // 50% chance of "Completed", 30% chance of "Failed", 20% chance remains "Pending"
            Random rnd = new Random();
            int chance = rnd.Next(1, 101); // 1 to 100

            if (chance <= 50)
                return "Completed";
            else if (chance <= 80)
                return "Failed";
            else
                return "Pending";
        }
    }
}
Registering Background Services in Program.cs

Next, we need to register the Payment Background service so that it will execute in the background. So, please add the following statement to the program class. We need to use the AddHostedService to register a background service.

// Register Background Service
builder.Services.AddHostedService<PendingPaymentService>();

Note: Irrespective of client Requests, background service automatically runs in the background when you start the application.

So, we have completed the payment module implementation for our e-commerce application using ASP.NET Core Web API and Entity Framework Core. In the next article, I will discuss How to Implement the Cancellation Module of our ECommerce Application. In this article, we discussed how to design and develop the payment module for our e-commerce application using ASP.NET Core Web API and EF Core. I hope you enjoyed the article.

Leave a Reply

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