Refund Module in Ecommerce Application

How to Design and Develop Refund Module in Ecommerce Application

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

The Refund Module in our E-commerce Application is designed to manage the process of returning funds to customers for canceled orders or returned products. It ensures accurate financial tracking and integrates smoothly with payment gateways like Stripe or PayPal. It includes functionality for handling refund requests, processing those refunds through integrated payment gateways, maintaining accurate financial records, and notifying customers about the refund status and completion.

Key Features of Refund Module

The following are the Key Features of the Refund Module that we will implement in our Ecommerce Application

  • Refund Requests: Facilitates handling refund requests linked to canceled orders or returned items.
  • Payment Integration: Processes refunds through integrated payment gateways (e.g., Stripe, PayPal, Net Banking, etc.), ensuring secure and efficient transactions.
  • Financial Tracking: Maintains accurate records of refund amounts and their statuses for accurate financial reporting.
  • Notification: Keeps customers informed about the status and completion of their refund requests through timely notifications.
Creating DTOs (Data Transfer Objects)

DTOs play a crucial role in the Refund Module by managing the data flow between the client and server. They ensure that only the necessary and secure data is exchanged, promoting encapsulation and reducing the risk of overexposing internal data structures. So, create a folder named RefundDTOs within the DTOs folder where we will create all the Required Request and Response DTOs for managing the Refund functionalities in our Ecommerce Application.

PendingRefundResponseDTO

Create a class file named PendingRefundResponseDTO.cs within the DTOs\RefundDTOs folder, then copy and paste the following code. This DTO is used by the endpoint that returns pending refunds, i.e., eligible cancellations without a refund record.

namespace ECommerceApp.DTOs.RefundDTOs
{
    // DTO for pending refund details.
    public class PendingRefundResponseDTO
    {
        public int CancellationId { get; set; }
        public int OrderId { get; set; }
        public decimal OrderAmount { get; set; }
        public decimal CancellationCharge { get; set; }
        public decimal ComputedRefundAmount { get; set; }
        public string? CancellationRemarks { get; set; }
    }
}
RefundRequestDTO

Create a class file named RefundRequestDTO.cs within the DTOs\RefundDTOs folder, then copy and paste the following code. The RefundRequestDTO initiates a refund request, typically associated with a cancellation or returned product. Ensures that the required fields are provided and meet validation criteria.

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

namespace ECommerceApp.DTOs.RefundDTOs
{
    // DTO for requesting a refund.
    public class RefundRequestDTO
    {
        [Required(ErrorMessage = "Cancellation ID is required.")]
        public int CancellationId { get; set; }

        [Required(ErrorMessage = "Refund Method is required.")]
        public RefundMethod RefundMethod { get; set; }

        [StringLength(500, ErrorMessage = "Refund Reason cannot exceed 500 characters.")]
        public string? RefundReason { get; set; }

        [Required(ErrorMessage = "ProcessedBy is required.")]
        public int ProcessedBy { get; set; }
    }
}
RefundResponseDTO

Create a class file named RefundResponseDTO.cs within the DTOs\RefundDTOs folder, then copy and paste the following code. The RefundResponseDTO encapsulates the details of a refund transaction, including identifiers, amounts, statuses, and timestamps. It ensures that only relevant refund information is returned to the client, maintaining data security and integrity.

using ECommerceApp.Models;
namespace ECommerceApp.DTOs.RefundDTOs
{
    // DTO for returning refund details.
    public class RefundResponseDTO
    {
        public int Id { get; set; }
        public int CancellationId { get; set; }
        public int PaymentId { get; set; }
        public decimal Amount { get; set; }
        public RefundStatus Status { get; set; }
        public DateTime InitiatedAt { get; set; }
        public DateTime? CompletedAt { get; set; }
        public string? TransactionId { get; set; }
        public RefundMethod RefundMethod { get; set; }
        public string? RefundReason { get; set; }
    }
}
RefundStatusUpdateDTO

Create a class file named RefundStatusUpdateDTO.cs within the DTOs\RefundDTOs folder, then copy and paste the following code. The RefundStatusUpdateDTO facilitates the manual updating of a refund’s status as completed.

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

namespace ECommerceApp.DTOs.RefundDTOs
{
    // DTO for updating refund status Manually
    public class RefundStatusUpdateDTO
    {
        [Required(ErrorMessage = "Refund ID is required.")]
        public int RefundId { get; set; }

        [StringLength(100, ErrorMessage = "Transaction ID cannot exceed 100 characters.")]
        [Required(ErrorMessage = "TransactionId is required.")]
        public string TransactionId { get; set; }

        [Required(ErrorMessage = "Refund Method is required.")]
        public RefundMethod RefundMethod { get; set; }

        [StringLength(500, ErrorMessage = "Refund Reason cannot exceed 500 characters.")]
        public string? RefundReason { get; set; }
        public int? ProcessedBy { get; set; }
    }
}
PaymentGatewayRefundResponseDTO

Create a class file named PaymentGatewayRefundResponseDTO.cs within the DTOs\RefundDTOs folder, then copy and paste the following code. The following DTO is used to simulate a payment gateway refund response.

using ECommerceApp.Models;
namespace ECommerceApp.DTOs.RefundDTOs
{
    // Helper class to simulate a payment gateway response.
    public class PaymentGatewayRefundResponseDTO
    {
        public bool IsSuccess { get; set; }
        public RefundStatus Status { get; set; }
        public string TransactionId { get; set; } 
    }
}
Updating RefundStatus Enum Values:

Now, if you look at the Statuses Master table in the database, you will see the following data.

Updating RefundStatus Enum Values

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

using System.Text.Json.Serialization;
namespace ECommerceApp.Models
{
    // Enum to represent the status of a refund
    [JsonConverter(typeof(JsonStringEnumConverter))]
    public enum RefundStatus
    {
        Pending = 1,
        Completed = 6,
        Failed = 7
    }
}
Create the Refund Service Class

The RefundService class encapsulates the business logic and data access for refund processing. It handles the creation, updating, retrieval, and pending refund listing. It also integrates with a simulated payment gateway and sends email notifications upon successful refunds. So, create a class file named RefundService.cs within the Services folder and then copy and paste the following code:

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

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

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

        // Retrieves all eligible cancellations for refund initiation.
        // (i.e. approved cancellations with no associated refund record)
        public async Task<ApiResponse<List<PendingRefundResponseDTO>>> GetEligibleRefundsAsync()
        {
            var eligible = await _context.Cancellations
                .Include(c => c.Order)
                    .ThenInclude(o => o.Payment)
                .Where(c => c.Status == CancellationStatus.Approved && c.Refund == null 
                            && c.Order.Payment.PaymentMethod.ToLower() != "COD")
                .Select(c => new PendingRefundResponseDTO
                {
                    CancellationId = c.Id,
                    OrderId = c.OrderId,
                    OrderAmount = c.OrderAmount,
                    CancellationCharge = c.CancellationCharges ?? 0.00m,
                    ComputedRefundAmount = c.OrderAmount - (c.CancellationCharges ?? 0.00m),
                    CancellationRemarks = c.Remarks
                }).ToListAsync();

            return new ApiResponse<List<PendingRefundResponseDTO>>(200, eligible);
        }

        // The ProcessRefundAsync method in the service will handles only those cancellations that are approved
        // and have no entry in the Refunds table.
        public async Task<ApiResponse<RefundResponseDTO>> ProcessRefundAsync(RefundRequestDTO refundRequest)
        {
            // Retrieve the cancellation with related Order, Payment, and Customer details.
            var cancellation = await _context.Cancellations
                .Include(c => c.Order)
                    .ThenInclude(o => o.Payment)
                .Include(c => c.Order)
                    .ThenInclude(o => o.Customer)
                .FirstOrDefaultAsync(c => c.Id == refundRequest.CancellationId);

            if (cancellation == null)
                return new ApiResponse<RefundResponseDTO>(404, "Cancellation request not found.");

            if (cancellation.Status != CancellationStatus.Approved)
                return new ApiResponse<RefundResponseDTO>(400, "Only approved cancellations are eligible for refunds.");

            // Only proceed if no refund record exists.
            var existingRefund = await _context.Refunds
                .FirstOrDefaultAsync(r => r.CancellationId == refundRequest.CancellationId);

            if (existingRefund != null)
                return new ApiResponse<RefundResponseDTO>(400, "Refund for this cancellation request has already been initiated.");

            // Validate that a Payment record exists.
            var payment = cancellation.Order.Payment;
            if (payment == null || payment.PaymentMethod.ToLower() == "COD")
                return new ApiResponse<RefundResponseDTO>(400, "No payment associated with the order.");

            // Compute the refund amount.
            decimal computedRefundAmount = cancellation.OrderAmount - (cancellation.CancellationCharges ?? 0.00m);
            if (computedRefundAmount <= 0)
                return new ApiResponse<RefundResponseDTO>(400, "Computed refund amount is not valid.");

            // Create a new refund record.
            var refund = new Refund
            {
                CancellationId = refundRequest.CancellationId,
                PaymentId = payment.Id,
                Amount = computedRefundAmount,
                RefundMethod = refundRequest.RefundMethod.ToString(),
                RefundReason = refundRequest.RefundReason,
                Status = RefundStatus.Pending,
                InitiatedAt = DateTime.UtcNow,
                ProcessedBy = refundRequest.ProcessedBy
            };

            _context.Refunds.Add(refund);
            await _context.SaveChangesAsync();

            // Immediately try processing via the simulated payment gateway.
            var gatewayResponse = await ProcessRefundPaymentAsync(refund);
            if (gatewayResponse.IsSuccess)
            {
                refund.Status = RefundStatus.Completed;
                refund.TransactionId = gatewayResponse.TransactionId;
                refund.CompletedAt = DateTime.UtcNow;

                payment.Status = PaymentStatus.Refunded;
                _context.Payments.Update(payment);

                // Send email notification.
                if (cancellation.Order.Customer != null && !string.IsNullOrEmpty(cancellation.Order.Customer.Email))
                {
                    await _emailService.SendEmailAsync(
                        cancellation.Order.Customer.Email,
                         $"Your Refund Has Been Processed Successfully, Order #{cancellation.Order.OrderNumber}",
                        GenerateRefundSuccessEmailBody(refund, cancellation.Order.OrderNumber, cancellation),
                        IsBodyHtml: true);
                }
            }
            else
            {
                refund.Status = RefundStatus.Failed;
            }

            _context.Refunds.Update(refund);
            await _context.SaveChangesAsync();

            return new ApiResponse<RefundResponseDTO>(200, MapRefundToDTO(refund));
        }

        // The UpdateRefundStatus method now allows an admin to manually process refunds that are either pending or failed.
        public async Task<ApiResponse<ConfirmationResponseDTO>> UpdateRefundStatusAsync(RefundStatusUpdateDTO statusUpdate)
        {
            var refund = await _context.Refunds
                .Include(r => r.Cancellation)
                    .ThenInclude(c => c.Order)
                        .ThenInclude(o => o.Customer)
                .Include(r => r.Payment)
                .FirstOrDefaultAsync(r => r.Id == statusUpdate.RefundId);

            if (refund == null)
                return new ApiResponse<ConfirmationResponseDTO>(404, "Refund not found.");

            // Allow manual update only if refund is Pending or Failed.
            if (refund.Status != RefundStatus.Pending && refund.Status != RefundStatus.Failed)
                return new ApiResponse<ConfirmationResponseDTO>(400, "Only pending or failed refunds can be updated.");

            // In a manual update, we reprocess the refund.
            refund.RefundMethod = statusUpdate.RefundMethod.ToString();
            refund.Status = RefundStatus.Completed;
            refund.TransactionId = statusUpdate.TransactionId;
            refund.CompletedAt = DateTime.UtcNow;
            refund.ProcessedBy = statusUpdate.ProcessedBy;
            refund.RefundReason = statusUpdate.RefundReason;

            //Also mark the Payment Status as Refunded
            refund.Payment.Status = PaymentStatus.Refunded;
            _context.Payments.Update(refund.Payment);
            _context.Refunds.Update(refund);
            await _context.SaveChangesAsync();

            // Send email notification.
            if (refund.Cancellation?.Order?.Customer != null && !string.IsNullOrEmpty(refund.Cancellation.Order.Customer.Email))
            {
                await _emailService.SendEmailAsync(
                    refund.Cancellation.Order.Customer.Email,
                    $"Your Refund Has Been Processed Successfully, Order #{refund.Cancellation.Order.OrderNumber}",
                    GenerateRefundSuccessEmailBody(refund, refund.Cancellation.Order.OrderNumber, refund.Cancellation),
                    IsBodyHtml: true);
            }

            var confirmation = new ConfirmationResponseDTO
            {
                Message = $"Refund with ID {refund.Id} has been updated to {refund.Status}."
            };

            return new ApiResponse<ConfirmationResponseDTO>(200, confirmation);
        }

        // Retrieves a refund by its ID.
        public async Task<ApiResponse<RefundResponseDTO>> GetRefundByIdAsync(int id)
        {
            var refund = await _context.Refunds
                .Include(r => r.Cancellation)
                    .ThenInclude(c => c.Order)
                        .ThenInclude(o => o.Payment)
                .FirstOrDefaultAsync(r => r.Id == id);

            if (refund == null)
                return new ApiResponse<RefundResponseDTO>(404, "Refund not found.");

            return new ApiResponse<RefundResponseDTO>(200, MapRefundToDTO(refund));
        }

        // Retrieves all refunds.
        public async Task<ApiResponse<List<RefundResponseDTO>>> GetAllRefundsAsync()
        {
            var refunds = await _context.Refunds
                .Include(r => r.Cancellation)
                    .ThenInclude(c => c.Order)
                        .ThenInclude(o => o.Payment)
                .ToListAsync();
            var refundList = refunds.Select(r => MapRefundToDTO(r)).ToList();
            return new ApiResponse<List<RefundResponseDTO>>(200, refundList);
        }

        // Private Helper Methods
        private RefundResponseDTO MapRefundToDTO(Refund refund)
        {
            return new RefundResponseDTO
            {
                Id = refund.Id,
                CancellationId = refund.CancellationId,
                PaymentId = refund.PaymentId,
                Amount = refund.Amount,
                RefundMethod = Enum.Parse<RefundMethod>(refund.RefundMethod),
                RefundReason = refund.RefundReason,
                Status = refund.Status,
                InitiatedAt = refund.InitiatedAt,
                CompletedAt = refund.CompletedAt,
                TransactionId = refund.TransactionId
            };
        }

        // Simulates calling a payment gateway to process the refund.
        // In production, replace this with actual integration code.
        public async Task<PaymentGatewayRefundResponseDTO> ProcessRefundPaymentAsync(Refund refund)
        {
            // Simulate a network delay of 1 second.
            await Task.Delay(TimeSpan.FromSeconds(1));

            // Create a Random instance. (In production, consider reusing a static instance.)
            var random = new Random();
            double chance = random.NextDouble(); // Generates a double between 0.0 and 1.0.

            if (chance < 0.70) // 70% chance for Completed.
            {
                return new PaymentGatewayRefundResponseDTO
                {
                    IsSuccess = true,
                    Status = RefundStatus.Completed,
                    TransactionId = $"TXN-{Guid.NewGuid().ToString().Substring(0, 8).ToUpper()}"
                };
            }
            else if (chance < 0.90) // Next 20% chance for Failed.
            {
                return new PaymentGatewayRefundResponseDTO
                {
                    IsSuccess = false,
                    Status = RefundStatus.Failed
                };
            }
            else // Remaining 10% chance for Pending.
            {
                return new PaymentGatewayRefundResponseDTO
                {
                    IsSuccess = false,
                    Status = RefundStatus.Pending
                };
            }
        }

        // Generates an HTML email body (with inline CSS) to notify the customer.
        public string GenerateRefundSuccessEmailBody(Refund refund, string orderNumber, Cancellation cancellation)
        {
            // Format CompletedAt if available; otherwise show "N/A".

            // Define the IST time zone.
            var istZone = TimeZoneInfo.FindSystemTimeZoneById("India Standard Time");

            // Convert CompletedAt from UTC to IST, if available.
            string completedAtStr = refund.CompletedAt.HasValue
                ? TimeZoneInfo.ConvertTimeFromUtc(refund.CompletedAt.Value, istZone).ToString("dd MMM yyyy HH:mm:ss")
                : "N/A";

            return $@"
            <html>
            <body style='font-family: Arial, sans-serif; margin: 0; padding: 0;'>
                <div style='background-color: #f4f4f4; padding: 20px;'>
                    <div style='max-width: 600px; margin: 0 auto; background-color: #ffffff; border: 1px solid #ddd;'>
                        <div style='padding: 20px; text-align: center; background-color: #2E86C1; color: #ffffff;'>
                            <h2>Your Refund is Complete</h2>
                        </div>
                        <div style='padding: 20px;'>
                            <p>Dear Customer,</p>
                            <p>Your refund has been processed successfully. Below are the details:</p>
                            <table style='width: 100%; border-collapse: collapse;'>
                                <tr>
                                    <td style='border: 1px solid #ddd; padding: 8px;'>Order Number</td>
                                    <td style='border: 1px solid #ddd; padding: 8px;'>{orderNumber}</td>
                                </tr>
                                <tr>
                                    <td style='border: 1px solid #ddd; padding: 8px;'>Refund Transaction ID</td>
                                    <td style='border: 1px solid #ddd; padding: 8px;'>{refund.TransactionId}</td>
                                </tr>
                                <tr>
                                    <td style='border: 1px solid #ddd; padding: 8px;'>Order Amount</td>
                                    <td style='border: 1px solid #ddd; padding: 8px;'>₹{cancellation.OrderAmount}</td>
                                </tr>
                                <tr>
                                    <td style='border: 1px solid #ddd; padding: 8px;'>Cancellation Charges</td>
                                    <td style='border: 1px solid #ddd; padding: 8px;'>₹{cancellation.CancellationCharges ?? 0.00m}</td>
                                </tr>
                                <tr>
                                    <td style='border: 1px solid #ddd; padding: 8px;'>Cancellation Reason</td>
                                    <td style='border: 1px solid #ddd; padding: 8px;'>{cancellation.Reason}</td>
                                </tr>
                                <tr>
                                    <td style='border: 1px solid #ddd; padding: 8px;'>Refunded Method</td>
                                    <td style='border: 1px solid #ddd; padding: 8px;'>{refund.RefundMethod}</td>
                                </tr>
                                <tr>
                                    <td style='border: 1px solid #ddd; padding: 8px;'>Refunded Amount</td>
                                    <td style='border: 1px solid #ddd; padding: 8px;'>₹{refund.Amount}</td>
                                </tr>
                                <tr>
                                    <td style='border: 1px solid #ddd; padding: 8px;'>CompletedAt At</td>
                                    <td style='border: 1px solid #ddd; padding: 8px;'>{completedAtStr}</td>
                                </tr>
                            </table>
                            <p>Thank you for shopping with us.</p>
                            <p>Best regards,<br/>The ECommerce Team</p>
                        </div>
                    </div>
                </div>
            </body>
            </html>";
        }
    }
}
Background Service for Processing Pending/Failed Refunds:

A background service (implemented as an IHostedService) periodically scans for refunds with a status of Pending or Failed and re‑attempts processing using the simulated payment gateway. The customer is notified by email when a refund is finally processed successfully. Create a new class file named RefundProcessingBackgroundService.cs in your Services folder, and then copy and paste the following code. This service runs periodically and re‑processes refunds that are pending or failed.

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

namespace ECommerceApp.Services
{
    public class RefundProcessingBackgroundService : BackgroundService
    {
        private readonly IServiceProvider _serviceProvider;
        // Run the background task every 5 minutes (adjust as needed).
        private readonly TimeSpan _interval = TimeSpan.FromMinutes(5);

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

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                await ProcessPendingAndFailedRefundsAsync(stoppingToken);
                await Task.Delay(_interval, stoppingToken);
            }
        }

        private async Task ProcessPendingAndFailedRefundsAsync(CancellationToken cancellationToken)
        {
            // Create a new scope to use scoped services.
            using (var scope = _serviceProvider.CreateScope())
            {
                var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
                var emailService = scope.ServiceProvider.GetRequiredService<EmailService>();
                var refundService = scope.ServiceProvider.GetRequiredService<RefundService>();

                // Query refunds with status Pending or Failed.
                var refunds = await context.Refunds
                    .Include(r => r.Cancellation)
                        .ThenInclude(c => c.Order)
                            .ThenInclude(o => o.Customer)
                    .Include(r => r.Payment)
                    .Where(r => r.Status == RefundStatus.Pending || r.Status == RefundStatus.Failed)
                    .ToListAsync(cancellationToken);

                foreach (var refund in refunds)
                {
                    var gatewayResponse = await refundService.ProcessRefundPaymentAsync(refund);
                    if (gatewayResponse.IsSuccess)
                    {
                        refund.Status = RefundStatus.Completed;
                        refund.TransactionId = gatewayResponse.TransactionId;
                        refund.CompletedAt = DateTime.UtcNow;
                        refund.Payment.Status = PaymentStatus.Refunded;
                        context.Payments.Update(refund.Payment);

                        context.Refunds.Update(refund);
                        await context.SaveChangesAsync(cancellationToken);

                        if (refund.Cancellation?.Order?.Customer != null &&
                            !string.IsNullOrEmpty(refund.Cancellation.Order.Customer.Email))
                        {
                            await emailService.SendEmailAsync(
                                refund.Cancellation.Order.Customer.Email,
                                 $"Your Refund Has Been Processed Successfully, Order #{refund.Cancellation.Order.OrderNumber}",
                                refundService.GenerateRefundSuccessEmailBody(refund, refund.Cancellation.Order.OrderNumber, refund.Cancellation),
                                IsBodyHtml: true);
                        }
                    }
                    else
                    {
                        refund.Status = gatewayResponse.Status;
                        refund.CompletedAt = DateTime.UtcNow;
                        await context.SaveChangesAsync(cancellationToken);
                    }
                }
            }
        }
    }
}
Registering the Refund and Refund Background Services:

Register the background and refund services in your DI container. So, please modify the Program class 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>();

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

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

            // Register Refund Processing Background Service
            builder.Services.AddHostedService<RefundProcessingBackgroundService>();
            
            // Register Payment Background Service
            builder.Services.AddHostedService<PendingPaymentService>();

            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 Refunds Controller

The RefundsController serves as the central point for managing refund-related operations within the Ecommerce Application. It handles incoming HTTP requests, interacts with the database, and returns appropriate responses. So, create a new API Empty Controller named RefundsController within the Controllers folder and then copy and paste the following code:

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

namespace ECommerceValidationDemo.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class RefundsController : ControllerBase
    {
        private readonly RefundService _refundService;

        public RefundsController(RefundService refundService)
        {
            _refundService = refundService;
        }

        // GET: api/Refunds/GetEligibleRefunds
        // Returns approved cancellations that have no associated refund entry.
        [HttpGet("GetEligibleRefunds")]
        public async Task<ActionResult<ApiResponse<List<PendingRefundResponseDTO>>>> GetEligibleRefunds()
        {
            var response = await _refundService.GetEligibleRefundsAsync();
            if (response.StatusCode != 200)
            {
                return StatusCode(response.StatusCode, response);
            }
            return Ok(response);
        }

        // POST: api/Refunds/ProcessRefund
        // Initiates a refund for approved cancellations without an existing refund record.
        [HttpPost("ProcessRefund")]
        public async Task<ActionResult<ApiResponse<RefundResponseDTO>>> ProcessRefund([FromBody] RefundRequestDTO refundRequest)
        {
            var response = await _refundService.ProcessRefundAsync(refundRequest);
            if (response.StatusCode != 200)
            {
                return StatusCode(response.StatusCode, response);
            }
            return Ok(response);
        }

        // PUT: api/Refunds/UpdateRefundStatus
        // Manually reprocesses a refund (only applicable if the refund is pending or failed).
        [HttpPut("UpdateRefundStatus")]
        public async Task<ActionResult<ApiResponse<ConfirmationResponseDTO>>> UpdateRefundStatus([FromBody] RefundStatusUpdateDTO statusUpdate)
        {
            var response = await _refundService.UpdateRefundStatusAsync(statusUpdate);
            if (response.StatusCode != 200)
            {
                return StatusCode(response.StatusCode, response);
            }
            return Ok(response);
        }

        // GET: api/Refunds/GetRefundById/{id}
        // Retrieves a refund by its ID.
        [HttpGet("GetRefundById/{id}")]
        public async Task<ActionResult<ApiResponse<RefundResponseDTO>>> GetRefundById(int id)
        {
            var response = await _refundService.GetRefundByIdAsync(id);
            if (response.StatusCode != 200)
            {
                return StatusCode(response.StatusCode, response);
            }
            return Ok(response);
        }

        // GET: api/Refunds/GetAllRefunds
        // Retrieves all refunds.
        [HttpGet("GetAllRefunds")]
        public async Task<ActionResult<ApiResponse<List<RefundResponseDTO>>>> GetAllRefunds()
        {
            var response = await _refundService.GetAllRefundsAsync();
            if (response.StatusCode != 200)
            {
                return StatusCode(response.StatusCode, response);
            }
            return Ok(response);
        }
    }
}
How to Test Each Refund Endpoint:

Let us test each Refund-related endpoint and the Refund Background Service of our Ecommerce Application. Please replace {port} with the port number your application is running on.

Get Eligible Refunds (Cancellations Needing Refund Initiation)

Method: GET
URL: https://localhost:{port}/api/Refunds/GetEligibleRefunds
Expected Outcome: Returns a JSON list of approved cancellations that do not yet have an associated refund record (i.e., refunds that need to be initiated). These are the ones waiting to be processed.

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

Process Refund

Method: POST
URL: https://localhost:{port}/api/Refunds/ProcessRefund
Headers: Content-Type: application/json
Body (raw JSON):

{
  "CancellationId": 1,
  "RefundMethod": "PayPal",
  "RefundReason": "Order Cancelled and Payment Made Online",
  "ProcessedBy": 1
} 

Expected Outcome: If cancellation ID 1 exists with the status Approved and has no refund record, a new refund is created and processed (or marked as failed if the simulated gateway returns failure). If a refund already exists, you’ll receive an error message.

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

Update Refund Status (Manual Processing)

Method: PUT
URL: https://localhost:{port}/api/Refunds/UpdateRefundStatus
Headers: Content-Type: application/json
Body (raw JSON):

{
  "RefundId": 3,
  "TransactionId": "TXN-ABCD1234",
  "RefundMethod": "PayPal",
  "RefundReason": "Manually processed after reattempt",
  "ProcessedBy": 1
}

Expected Outcome: If refund ID 3 is in Pending or Failed status, the service will attempt to process it manually and update its status accordingly. A confirmation message with the updated status is returned.

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

Get a Refund by ID

Method: GET
URL: https://localhost:{port}/api/Refunds/GetRefundById/2
Expected Outcome: Returns the refund details with ID 2 in JSON format.

Refund Module in ECommerce Application

Get All Refunds

Method: GET
URL: https://localhost:{port}/api/Refunds/GetAllRefunds
Expected Outcome: Returns a JSON list of all refund records.

How to Test the Pending Refund Background Service

How to Test the Pending Refund Background Service:

The background service (PendingRefundBackgroundService) runs every 5 minutes. To test it, create a refund record with the status Pending or Failed (for example, by manually inserting a record or triggering a pending refund). Then, wait for the background service interval and check the database (or logs) to see that the refund’s status is updated and that an email was sent if the refund was completed successfully.

So, we have completed the Refund 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 User Feedback Module of our ECommerce Application. In this article, we discussed how to design and develop the refund module for our e-commerce application. I hope you enjoyed the article.

Leave a Reply

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