From 0e84bae0e1d01f3e64e723e1013f03cac72ab320 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 17 Feb 2026 08:50:12 +0000
Subject: [PATCH 1/4] Initial plan
From ea99f6474004d85eff675a16aca23241d7a7d097 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 17 Feb 2026 08:53:11 +0000
Subject: [PATCH 2/4] Add contract and service for customers with overdue
orders
Co-authored-by: florinc <443676+florinc@users.noreply.github.com>
---
.../Sales/CustomerWithOverdueOrdersData.cs | 22 +++++++++++++++++
Modules/Contracts/Sales/ICustomerService.cs | 10 ++++++++
.../Sales/Sales.Services/CustomerService.cs | 24 +++++++++++++++++++
3 files changed, 56 insertions(+)
create mode 100644 Modules/Contracts/Sales/CustomerWithOverdueOrdersData.cs
diff --git a/Modules/Contracts/Sales/CustomerWithOverdueOrdersData.cs b/Modules/Contracts/Sales/CustomerWithOverdueOrdersData.cs
new file mode 100644
index 0000000..e2c2593
--- /dev/null
+++ b/Modules/Contracts/Sales/CustomerWithOverdueOrdersData.cs
@@ -0,0 +1,22 @@
+namespace Contracts.Sales;
+
+///
+/// Customer information with overdue order statistics
+///
+public sealed class CustomerWithOverdueOrdersData
+{
+ ///
+ /// Customer display name (CompanyName if available, otherwise FirstName + LastName)
+ ///
+ public required string CustomerName { get; init; }
+
+ ///
+ /// Total count of overdue orders for this customer
+ ///
+ public int OverdueOrdersCount { get; init; }
+
+ ///
+ /// Due date of the oldest overdue order
+ ///
+ public DateTime OldestDueDate { get; init; }
+}
diff --git a/Modules/Contracts/Sales/ICustomerService.cs b/Modules/Contracts/Sales/ICustomerService.cs
index 338392c..bc026f8 100644
--- a/Modules/Contracts/Sales/ICustomerService.cs
+++ b/Modules/Contracts/Sales/ICustomerService.cs
@@ -7,4 +7,14 @@ public interface ICustomerService
CustomerData[] GetCustomersWithOrdersStartingWith(string prefix);
CustomerData[] GetCustomersWithOrdersContaining(string fragment);
+
+ ///
+ /// Retrieves customers that have at least one overdue order.
+ /// An order is overdue when DueDate < Today and Status is not Shipped or Cancelled.
+ ///
+ ///
+ /// Array of customers with overdue order information, ordered by oldest overdue date ascending.
+ /// Returns empty array if no customers with overdue orders exist.
+ ///
+ CustomerWithOverdueOrdersData[] GetCustomersWithOverdueOrders();
}
diff --git a/Modules/Sales/Sales.Services/CustomerService.cs b/Modules/Sales/Sales.Services/CustomerService.cs
index 7b49222..25e2f8c 100644
--- a/Modules/Sales/Sales.Services/CustomerService.cs
+++ b/Modules/Sales/Sales.Services/CustomerService.cs
@@ -4,6 +4,7 @@
using Contracts.Sales;
using DataAccess;
using Sales.DataModel.SalesLT;
+using Sales.DataModel.Values;
namespace Sales.Services;
@@ -52,4 +53,27 @@ public CustomerData[] GetCustomersWithOrdersContaining(string fragment)
return GetCustomersWithOrdersFilteredBy(filter);
}
+
+ public CustomerWithOverdueOrdersData[] GetCustomersWithOverdueOrders()
+ {
+ var today = DateTime.Today;
+ var closedStatuses = new[] { SalesOrderHeaderStatusValues.Shipped, SalesOrderHeaderStatusValues.Cancelled };
+
+ return repository.GetEntities()
+ .Where(c => c.SalesOrderHeaders.Any(o =>
+ o.DueDate < today &&
+ !closedStatuses.Contains(o.Status)))
+ .Select(c => new CustomerWithOverdueOrdersData
+ {
+ CustomerName = c.CompanyName ?? $"{c.FirstName} {c.LastName}",
+ OverdueOrdersCount = c.SalesOrderHeaders
+ .Where(o => o.DueDate < today && !closedStatuses.Contains(o.Status))
+ .Count(),
+ OldestDueDate = c.SalesOrderHeaders
+ .Where(o => o.DueDate < today && !closedStatuses.Contains(o.Status))
+ .Min(o => o.DueDate)
+ })
+ .OrderBy(x => x.OldestDueDate)
+ .ToArray();
+ }
}
From fd3675c6ee2224f0c5fc9fd23d86135ca08feabe Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 17 Feb 2026 08:53:42 +0000
Subject: [PATCH 3/4] Add console command for customers with overdue orders
Co-authored-by: florinc <443676+florinc@users.noreply.github.com>
---
...ustomersWithOverdueOrdersConsoleCommand.cs | 31 +++++++++++++++++++
1 file changed, 31 insertions(+)
create mode 100644 Modules/Sales/Sales.ConsoleCommands/CustomersWithOverdueOrdersConsoleCommand.cs
diff --git a/Modules/Sales/Sales.ConsoleCommands/CustomersWithOverdueOrdersConsoleCommand.cs b/Modules/Sales/Sales.ConsoleCommands/CustomersWithOverdueOrdersConsoleCommand.cs
new file mode 100644
index 0000000..636880b
--- /dev/null
+++ b/Modules/Sales/Sales.ConsoleCommands/CustomersWithOverdueOrdersConsoleCommand.cs
@@ -0,0 +1,31 @@
+using AppBoot.DependencyInjection;
+using Contracts.ConsoleUi;
+using Contracts.Sales;
+
+namespace Sales.ConsoleCommands;
+
+[Service(typeof(IConsoleCommand))]
+internal sealed class CustomersWithOverdueOrdersConsoleCommand(IConsole console, ICustomerService customerService) : IConsoleCommand
+{
+ public string MenuLabel => "Show customers with overdue orders";
+
+ public void Execute()
+ {
+ console.WriteLine("Retrieving customers with overdue orders...");
+
+ var customers = customerService.GetCustomersWithOverdueOrders();
+
+ if (customers.Length == 0)
+ {
+ console.WriteLine("No customers with overdue orders found.");
+ return;
+ }
+
+ console.WriteLine($"Found {customers.Length} customer(s) with overdue orders:\n");
+
+ foreach (var customer in customers)
+ {
+ console.WriteEntity(customer);
+ }
+ }
+}
From 54f7fd7817cb35a3347bed4ea665c5653a64c90d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 17 Feb 2026 08:57:07 +0000
Subject: [PATCH 4/4] Add comprehensive unit tests for
GetCustomersWithOverdueOrders
Co-authored-by: florinc <443676+florinc@users.noreply.github.com>
---
.../CustomerServiceTests.cs | 212 ++++++++++++++++++
.../Fakes/FakeRepository.cs | 28 +++
.../Sales.Services.UnitTests.csproj | 31 +++
.../Sales.Services/Sales.Services.csproj | 4 +
4 files changed, 275 insertions(+)
create mode 100644 Modules/Sales/Sales.Services.UnitTests/CustomerServiceTests.cs
create mode 100644 Modules/Sales/Sales.Services.UnitTests/Fakes/FakeRepository.cs
create mode 100644 Modules/Sales/Sales.Services.UnitTests/Sales.Services.UnitTests.csproj
diff --git a/Modules/Sales/Sales.Services.UnitTests/CustomerServiceTests.cs b/Modules/Sales/Sales.Services.UnitTests/CustomerServiceTests.cs
new file mode 100644
index 0000000..5d2fff6
--- /dev/null
+++ b/Modules/Sales/Sales.Services.UnitTests/CustomerServiceTests.cs
@@ -0,0 +1,212 @@
+using Contracts.Sales;
+using Sales.DataModel.SalesLT;
+using Sales.DataModel.Values;
+using Sales.Services.UnitTests.Fakes;
+
+namespace Sales.Services.UnitTests;
+
+public class CustomerServiceTests
+{
+ [Fact]
+ public void GetCustomersWithOverdueOrders_WithNoCustomers_ReturnsEmptyArray()
+ {
+ var target = GetTarget(new Customer[0]);
+
+ var result = target.GetCustomersWithOverdueOrders();
+
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public void GetCustomersWithOverdueOrders_WithNoOverdueOrders_ReturnsEmptyArray()
+ {
+ var customer = CreateCustomer(1, "Acme", "John", "Doe",
+ CreateOrder(1, DateTime.Today.AddDays(10), SalesOrderHeaderStatusValues.InProcess));
+ var target = GetTarget(new[] { customer });
+
+ var result = target.GetCustomersWithOverdueOrders();
+
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public void GetCustomersWithOverdueOrders_WithOneCustomerOneOverdueOrder_ReturnsCustomer()
+ {
+ var dueDate = DateTime.Today.AddDays(-5);
+ var customer = CreateCustomer(1, "Acme Corp", "John", "Doe",
+ CreateOrder(1, dueDate, SalesOrderHeaderStatusValues.InProcess));
+ var target = GetTarget(new[] { customer });
+
+ var result = target.GetCustomersWithOverdueOrders();
+
+ Assert.Single(result);
+ Assert.Equal("Acme Corp", result[0].CustomerName);
+ Assert.Equal(1, result[0].OverdueOrdersCount);
+ Assert.Equal(dueDate, result[0].OldestDueDate);
+ }
+
+ [Fact]
+ public void GetCustomersWithOverdueOrders_WithMultipleCustomers_ReturnsSortedByOldestDueDate()
+ {
+ var customer1 = CreateCustomer(1, "Beta Inc", "Jane", "Smith",
+ CreateOrder(1, DateTime.Today.AddDays(-3), SalesOrderHeaderStatusValues.InProcess));
+ var customer2 = CreateCustomer(2, "Alpha Corp", "Bob", "Jones",
+ CreateOrder(2, DateTime.Today.AddDays(-10), SalesOrderHeaderStatusValues.Approved));
+ var customer3 = CreateCustomer(3, "Gamma LLC", "Alice", "Brown",
+ CreateOrder(3, DateTime.Today.AddDays(-5), SalesOrderHeaderStatusValues.Backordered));
+ var target = GetTarget(new[] { customer1, customer2, customer3 });
+
+ var result = target.GetCustomersWithOverdueOrders();
+
+ Assert.Equal(3, result.Length);
+ Assert.Equal("Alpha Corp", result[0].CustomerName);
+ Assert.Equal("Gamma LLC", result[1].CustomerName);
+ Assert.Equal("Beta Inc", result[2].CustomerName);
+ }
+
+ [Fact]
+ public void GetCustomersWithOverdueOrders_ExcludesShippedOrders_WhenDueDatePassed()
+ {
+ var customer = CreateCustomer(1, "Test Co", "John", "Doe",
+ CreateOrder(1, DateTime.Today.AddDays(-5), SalesOrderHeaderStatusValues.Shipped));
+ var target = GetTarget(new[] { customer });
+
+ var result = target.GetCustomersWithOverdueOrders();
+
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public void GetCustomersWithOverdueOrders_ExcludesCancelledOrders_WhenDueDatePassed()
+ {
+ var customer = CreateCustomer(1, "Test Co", "John", "Doe",
+ CreateOrder(1, DateTime.Today.AddDays(-5), SalesOrderHeaderStatusValues.Cancelled));
+ var target = GetTarget(new[] { customer });
+
+ var result = target.GetCustomersWithOverdueOrders();
+
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public void GetCustomersWithOverdueOrders_IncludesInProcessOrders_WhenDueDatePassed()
+ {
+ var customer = CreateCustomer(1, "Test Co", "John", "Doe",
+ CreateOrder(1, DateTime.Today.AddDays(-5), SalesOrderHeaderStatusValues.InProcess));
+ var target = GetTarget(new[] { customer });
+
+ var result = target.GetCustomersWithOverdueOrders();
+
+ Assert.Single(result);
+ }
+
+ [Fact]
+ public void GetCustomersWithOverdueOrders_ExcludesOrdersNotYetDue_EvenIfNotClosed()
+ {
+ var customer = CreateCustomer(1, "Test Co", "John", "Doe",
+ CreateOrder(1, DateTime.Today.AddDays(5), SalesOrderHeaderStatusValues.InProcess));
+ var target = GetTarget(new[] { customer });
+
+ var result = target.GetCustomersWithOverdueOrders();
+
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public void GetCustomersWithOverdueOrders_CalculatesCountCorrectly_ForMultipleOverdueOrders()
+ {
+ var oldestDueDate = DateTime.Today.AddDays(-10);
+ var customer = CreateCustomer(1, "Test Co", "John", "Doe",
+ CreateOrder(1, oldestDueDate, SalesOrderHeaderStatusValues.InProcess),
+ CreateOrder(2, DateTime.Today.AddDays(-5), SalesOrderHeaderStatusValues.Approved),
+ CreateOrder(3, DateTime.Today.AddDays(-2), SalesOrderHeaderStatusValues.Backordered));
+ var target = GetTarget(new[] { customer });
+
+ var result = target.GetCustomersWithOverdueOrders();
+
+ Assert.Single(result);
+ Assert.Equal(3, result[0].OverdueOrdersCount);
+ Assert.Equal(oldestDueDate, result[0].OldestDueDate);
+ }
+
+ [Fact]
+ public void GetCustomersWithOverdueOrders_UsesCompanyName_WhenAvailable()
+ {
+ var customer = CreateCustomer(1, "Acme Corporation", "John", "Doe",
+ CreateOrder(1, DateTime.Today.AddDays(-5), SalesOrderHeaderStatusValues.InProcess));
+ var target = GetTarget(new[] { customer });
+
+ var result = target.GetCustomersWithOverdueOrders();
+
+ Assert.Single(result);
+ Assert.Equal("Acme Corporation", result[0].CustomerName);
+ }
+
+ [Fact]
+ public void GetCustomersWithOverdueOrders_CombinesFirstAndLastName_WhenCompanyNameNull()
+ {
+ var customer = CreateCustomer(1, null, "Jane", "Smith",
+ CreateOrder(1, DateTime.Today.AddDays(-5), SalesOrderHeaderStatusValues.InProcess));
+ var target = GetTarget(new[] { customer });
+
+ var result = target.GetCustomersWithOverdueOrders();
+
+ Assert.Single(result);
+ Assert.Equal("Jane Smith", result[0].CustomerName);
+ }
+
+ [Fact]
+ public void GetCustomersWithOverdueOrders_MixedOrderStatuses_ReturnsOnlyOverdue()
+ {
+ var oldestOverdue = DateTime.Today.AddDays(-8);
+ var customer = CreateCustomer(1, "Test Co", "John", "Doe",
+ CreateOrder(1, oldestOverdue, SalesOrderHeaderStatusValues.InProcess),
+ CreateOrder(2, DateTime.Today.AddDays(-5), SalesOrderHeaderStatusValues.Shipped),
+ CreateOrder(3, DateTime.Today.AddDays(-3), SalesOrderHeaderStatusValues.Approved),
+ CreateOrder(4, DateTime.Today.AddDays(5), SalesOrderHeaderStatusValues.InProcess));
+ var target = GetTarget(new[] { customer });
+
+ var result = target.GetCustomersWithOverdueOrders();
+
+ Assert.Single(result);
+ Assert.Equal(2, result[0].OverdueOrdersCount);
+ Assert.Equal(oldestOverdue, result[0].OldestDueDate);
+ }
+
+ private static CustomerService GetTarget(Customer[] customers)
+ {
+ var repositoryStub = new FakeRepository(customers);
+ return new CustomerService(repositoryStub);
+ }
+
+ private static Customer CreateCustomer(int id, string? companyName, string firstName, string lastName, params SalesOrderHeader[] orders)
+ {
+ var customer = new Customer
+ {
+ CustomerID = id,
+ CompanyName = companyName,
+ FirstName = firstName,
+ LastName = lastName
+ };
+
+ foreach (var order in orders)
+ {
+ customer.SalesOrderHeaders.Add(order);
+ order.Customer = customer;
+ order.CustomerID = id;
+ }
+
+ return customer;
+ }
+
+ private static SalesOrderHeader CreateOrder(int id, DateTime dueDate, byte status)
+ {
+ return new SalesOrderHeader
+ {
+ SalesOrderID = id,
+ DueDate = dueDate,
+ Status = status,
+ OrderDate = DateTime.Today.AddDays(-30)
+ };
+ }
+}
diff --git a/Modules/Sales/Sales.Services.UnitTests/Fakes/FakeRepository.cs b/Modules/Sales/Sales.Services.UnitTests/Fakes/FakeRepository.cs
new file mode 100644
index 0000000..3d26ddb
--- /dev/null
+++ b/Modules/Sales/Sales.Services.UnitTests/Fakes/FakeRepository.cs
@@ -0,0 +1,28 @@
+using Sales.DataModel.SalesLT;
+
+namespace Sales.Services.UnitTests.Fakes;
+
+internal class FakeRepository : DataAccess.IRepository
+{
+ private readonly List _customers = new();
+
+ public FakeRepository(Customer[] customers)
+ {
+ _customers.AddRange(customers);
+ }
+
+ public IQueryable GetEntities() where TEntity : class
+ {
+ if (typeof(TEntity) == typeof(Customer))
+ {
+ return (IQueryable)_customers.AsQueryable();
+ }
+
+ throw new NotImplementedException($"Entity type {typeof(TEntity).Name} not supported in FakeRepository");
+ }
+
+ public DataAccess.IUnitOfWork CreateUnitOfWork()
+ {
+ throw new NotImplementedException("CreateUnitOfWork not needed for read-only tests");
+ }
+}
diff --git a/Modules/Sales/Sales.Services.UnitTests/Sales.Services.UnitTests.csproj b/Modules/Sales/Sales.Services.UnitTests/Sales.Services.UnitTests.csproj
new file mode 100644
index 0000000..6f7b31f
--- /dev/null
+++ b/Modules/Sales/Sales.Services.UnitTests/Sales.Services.UnitTests.csproj
@@ -0,0 +1,31 @@
+
+
+
+ net10.0
+ enable
+ enable
+ Exe
+ false
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Modules/Sales/Sales.Services/Sales.Services.csproj b/Modules/Sales/Sales.Services/Sales.Services.csproj
index 68dae67..8c798f9 100644
--- a/Modules/Sales/Sales.Services/Sales.Services.csproj
+++ b/Modules/Sales/Sales.Services/Sales.Services.csproj
@@ -7,6 +7,10 @@
true
+
+
+
+