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 + + + +