From c8922a6303e96354c24a0e9fa4cfcf714c7095f8 Mon Sep 17 00:00:00 2001 From: Florin Coros Date: Thu, 19 Mar 2026 16:19:56 +0200 Subject: [PATCH 1/5] Refine code-reviewer agent and change the code-review-report skill to be only as Markdown --- .github/agents/code-reviewer.agent.md | 12 +- .github/copilot-instructions.md | 2 +- .../SKILL.md | 154 ++---------------- 3 files changed, 25 insertions(+), 143 deletions(-) rename .github/skills/{code-review-report => code-review-md-report}/SKILL.md (51%) diff --git a/.github/agents/code-reviewer.agent.md b/.github/agents/code-reviewer.agent.md index 10286d4..5f3b525 100644 --- a/.github/agents/code-reviewer.agent.md +++ b/.github/agents/code-reviewer.agent.md @@ -127,6 +127,7 @@ Enforce rules from `.github/copilot-instructions.md`: ### 2. Read Implementation - Read every file in the provided file list +- Map each file to the commit_id from the provided commits list - For each file, understand its role (contract, service, entity, interceptor, test, etc.) - If build/test status failed, note failures upfront @@ -173,9 +174,16 @@ Output a summary as following: ### Review Report -Output a structured review report in markdown format, and save it at `docs/code-reviews/{issueId}-code-reviewer_{timestamp}.md`. +Output a structured review report in the requested format. Save it at `docs/code-reviews/{issueId}-code-review_{timestamp}.{extension}`. + +Use below table to determine the skill you will use to generate the report: + +| Format | Skill | Extension | +|--------|-------|-----------| +| markdown | `code-review-md-report` | md | +| json | `code-review-json-report` | json | +| yaml | `code-review-yaml-report` | yaml | -Use the `code-review-report` skill to generate the report. In case of not being able to use the skill, report a error and produce a simple markdown report. ## Error Recovery diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 1721529..ebaa629 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -212,7 +212,7 @@ Domain-specific guidance documents located in `.github/skills/`. When a task mat |-------|---------| | **add-module-plugin** | Step-by-step guide for adding new modules: folder structure, plugin registration, `IModule` initialization, build-order dependencies in `.sln` | | **unit-testing** | Test patterns using xUnit, NSubstitute, FluentAssertions: AAA structure, fake naming (`Stub`/`Mock`), collection assertions, `GetTarget` helpers | -| **code-review-report** | Structured code review report template for PR-importable Markdown output (GitHub / Azure DevOps compatible) | +| **code-review-md-report** | Structured code review report template for PR-importable Markdown output (GitHub / Azure DevOps compatible) | Usage pattern in this file: `> For [topic], use the **skill-name** skill.` diff --git a/.github/skills/code-review-report/SKILL.md b/.github/skills/code-review-md-report/SKILL.md similarity index 51% rename from .github/skills/code-review-report/SKILL.md rename to .github/skills/code-review-md-report/SKILL.md index efa991f..b4133b7 100644 --- a/.github/skills/code-review-report/SKILL.md +++ b/.github/skills/code-review-md-report/SKILL.md @@ -1,40 +1,33 @@ --- -name: code-review-report -description: "Generate structured code review reports in Markdown format suitable for import into GitHub PRs or Azure DevOps pull requests. Produces per-file inline remarks and a summary that renders natively in PR comment threads." -version: 2.0.0 -output_format: Markdown + JSON -platforms: GitHub, Azure DevOps +name: code-review-md-report +description: "Generate structured code review reports in Markdown format. Produces per-file inline remarks and a summary" +version: 1.0.0 +output_format: Markdown --- -# Code Review Report Skill +# Code Review Markdown Report Skill ## Purpose -Generates a structured, portable code review report consisting of **two artifacts**: +Generates a structured, portable code review report consisting of **one artifact**: 1. **Markdown report** — human-readable, renders correctly as a GitHub or Azure DevOps PR comment; can be pasted directly or posted via `gh pr comment` -2. **JSON payload** — machine-ready structured data for programmatic import via `gh api` (GitHub) or `curl` (Azure DevOps REST API) -Both artifacts are saved to `docs/code-reviews/` and referenced in the chat summary. +The artifact is saved to `docs/code-reviews/` and referenced in the chat summary. ## When to Use - After completing a code review with the `code-reviewer` agent -- When the review output must be shared in a pull request on GitHub or Azure DevOps +- When the review output is requested as a markdown report (e.g., "Generate a markdown code review report ...") - When a persistent review artifact is needed for traceability -## Output Files +## Output File -| File | Path | Purpose | -|------|------|---------| -| Markdown report | `docs/code-reviews/{issueId}-review_{timestamp}.md` | Human-readable, paste into PR comment | -| JSON payload | `docs/code-reviews/{issueId}-review_{timestamp}.json` | Programmatic PR import (GitHub + Azure DevOps) | +Markdown report: `docs/code-reviews/{issueId}-code-review_{timestamp}.md` - Human-readable, paste into PR comment **Timestamp format:** `yyyyMMdd-HHmm` (e.g., `20260225-1430`) -> Generate the JSON payload **only** when at least one remark has a resolvable file path. If all remarks are general (no file/line), produce only the Markdown report. - -## Artifact 1 — Markdown Report Template +## Markdown Report Template Use the following template **exactly** as the structure for the report file. Replace all `{placeholders}` with actual values. Omit sections that have no content (e.g., if no blockers, omit the blockers subsection) but always keep the top-level sections. @@ -87,6 +80,7 @@ Use the following template **exactly** as the structure for the report file. Rep #### R{number}: {short title} - **File:** `{relative/path/to/File.cs}` +- **Commit:** `{commit-id}` - **Line(s):** {line or range} - **Dimension:** {Design Conformance | Architecture Compliance | Code Quality | Test Quality | Scope & Hygiene} - **Description:** {What is wrong and why it matters} @@ -101,6 +95,7 @@ Use the following template **exactly** as the structure for the report file. Rep #### R{number}: {short title} - **File:** `{relative/path/to/File.cs}` +- **Commit:** `{commit-id}` - **Line(s):** {line or range} - **Dimension:** {dimension} - **Description:** {description} @@ -115,6 +110,7 @@ Use the following template **exactly** as the structure for the report file. Rep #### R{number}: {short title} - **File:** `{relative/path/to/File.cs}` +- **Commit:** `{commit-id}` - **Line(s):** {line or range} - **Dimension:** {dimension} - **Description:** {description} @@ -162,115 +158,6 @@ Use the following template **exactly** as the structure for the report file. Rep --- -## Artifact 2 — JSON Payload Template - -The JSON payload serves a dual purpose: it is the body for the **GitHub PR Review API** and contains all data needed to reconstruct **Azure DevOps PR threads**. The agent fills this structure from the collected remarks. - -```json -{ - "_meta": { - "issueId": "{issueId}", - "issueTitle": "{issueTitle}", - "date": "{date}", - "timestamp": "{timestamp}", - "markdownReport": "docs/code-reviews/{issueId}-review_{timestamp}.md" - }, - "verdict": "{APPROVE|REQUEST_CHANGES|COMMENT}", - "summary": "{2-4 sentence overview}", - "body": "{full markdown summary block — same as the Markdown report's Summary + Metrics + Verdict sections}", - "event": "{APPROVE|REQUEST_CHANGES|COMMENT}", - "metrics": { - "blockers": 0, - "warnings": 0, - "suggestions": 0, - "notes": 0 - }, - "remarks": [ - { - "id": "R1", - "severity": "blocker", - "dimension": "Architecture Compliance", - "file": "Modules/Sales/Sales.Services/OrderingService.cs", - "line": 42, - "lineEnd": 42, - "title": "{short title}", - "description": "{full description}", - "suggestedFix": "{direction — no full code}" - } - ] -} -``` - -### JSON Field Rules - -| Field | Values | Notes | -|-------|--------|-------| -| `verdict` / `event` | `APPROVE`, `REQUEST_CHANGES`, `COMMENT` | `APPROVE WITH SUGGESTIONS` → use `COMMENT` | -| `severity` | `blocker`, `warning`, `suggestion`, `note` | Lowercase | -| `file` | Repo-root relative, forward slashes | e.g. `Modules/Sales/Sales.Services/Foo.cs` | -| `line` / `lineEnd` | Integer (1-based), or `null` if unknown | Set both to same value for single-line remarks | -| `suggestedFix` | Omit the field if remark is a `note` | | -| `body` | GFM Markdown string | Used as the top-level PR review body | - ---- - -## CLI Import Commands - -After the agent saves both files, output a **ready-to-run commands block** in the chat so the user can import the review into their PR immediately. - -### GitHub — `gh` CLI - -```bash -# Option A: Post the full Markdown report as a single PR comment -gh pr comment {PR_NUMBER} --body-file docs/code-reviews/{issueId}-review_{timestamp}.md - -# Option B: Post as a formal PR review with inline file comments (uses GitHub Review API) -# Requires: GH_TOKEN with repo scope, jq installed -gh api repos/{OWNER}/{REPO}/pulls/{PR_NUMBER}/reviews \ - --method POST \ - --input docs/code-reviews/{issueId}-review_{timestamp}.json -``` - -> **Note for Option B:** The JSON payload's top-level structure matches the GitHub PR Review API. -> GitHub inline comments (`remarks[]`) require `path` (= `file`), `line`, and `body`. -> The `gh api --input` flag reads JSON directly from the file — no shell escaping needed. - -### Azure DevOps — REST API via `curl` - -```bash -# Post the full Markdown report as a PR thread (top-level comment) -curl -s -X POST \ - "https://dev.azure.com/{ORG}/{PROJECT}/_apis/git/repositories/{REPO}/pullRequests/{PR_ID}/threads?api-version=7.1" \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer $AZURE_DEVOPS_TOKEN" \ - --data-binary @- < The agent outputs **placeholder-filled** commands (with actual values substituted) so the user only needs to set environment variables (`OWNER`, `REPO`, `PR_NUMBER`, `AZURE_DEVOPS_TOKEN`) and run. - ---- - ## Formatting Rules ### General @@ -293,19 +180,6 @@ curl -s -X POST \ - Range: `L42-L58` - If line number cannot be determined, use `(general)` and explain the scope in the description -### GitHub PR Compatibility -The report is designed to render correctly when pasted as a GitHub PR comment: -- Tables use standard GitHub-flavored Markdown (GFM) -- Emoji severities (🔴🟡🟢ℹ️) render natively -- No HTML tags — pure Markdown only -- Collapsible sections are NOT used (inconsistent support across platforms) -- Code references use backtick-wrapped paths, not links (links break across forks) - -### Azure DevOps PR Compatibility -- Azure DevOps supports GFM in PR comments -- Same Markdown renders correctly in both platforms -- Avoid Mermaid diagrams in the report (not supported in Azure DevOps PR comments) - ## Edge Cases ### No Remarks Found From 2ceb85754cd95df89df84b4ea9a3e151bf4bb211 Mon Sep 17 00:00:00 2001 From: Florin Coros Date: Thu, 19 Mar 2026 17:34:27 +0200 Subject: [PATCH 2/5] Remove model spec from each agent.md file. It will be specified in the CLI when executing the model --- .github/agents/architect.agent.md | 1 - .github/agents/code-reviewer.agent.md | 3 +-- .github/agents/coder.agent.md | 1 - .github/agents/detailed-designer.agent.md | 1 - .github/skills/code-review-md-report/SKILL.md | 2 +- 5 files changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/agents/architect.agent.md b/.github/agents/architect.agent.md index edd0702..7eb963b 100644 --- a/.github/agents/architect.agent.md +++ b/.github/agents/architect.agent.md @@ -1,7 +1,6 @@ --- description: 'Architecture design agent for plugin-based modular systems following Clean Architecture principles with strict boundary enforcement' tools: ['execute/getTerminalOutput', 'execute/runTask', 'execute/getTaskOutput', 'execute/createAndRunTask', 'execute/runInTerminal', 'read/getNotebookSummary', 'read/readFile', 'edit/createDirectory', 'edit/createFile', 'edit/editFiles', 'edit/editNotebook', 'github/issue_read'] -model: Claude Sonnet 4.5 (copilot) handoffs: - label: Review Architecture Design agent: architect diff --git a/.github/agents/code-reviewer.agent.md b/.github/agents/code-reviewer.agent.md index 5f3b525..f0dbbf9 100644 --- a/.github/agents/code-reviewer.agent.md +++ b/.github/agents/code-reviewer.agent.md @@ -1,7 +1,6 @@ --- description: 'Code review agent that verifies implementations match design documents, enforce architectural constraints, and meet quality standards' tools: ['execute/getTerminalOutput', 'execute/runTask', 'execute/getTaskOutput', 'execute/createAndRunTask', 'execute/runInTerminal', 'read/getNotebookSummary', 'read/readFile', 'edit/createDirectory', 'edit/createFile', 'edit/editFiles', 'edit/editNotebook', 'github/issue_read'] -model: Claude Sonnet 4.6 (copilot) required_skills: - path: '.github/skills/code-review-report/SKILL.md' when: 'always' @@ -127,7 +126,7 @@ Enforce rules from `.github/copilot-instructions.md`: ### 2. Read Implementation - Read every file in the provided file list -- Map each file to the commit_id from the provided commits list +- Map each file to the commit_id from the provided commits list for context - For each file, understand its role (contract, service, entity, interceptor, test, etc.) - If build/test status failed, note failures upfront diff --git a/.github/agents/coder.agent.md b/.github/agents/coder.agent.md index f0cd729..ddf94eb 100644 --- a/.github/agents/coder.agent.md +++ b/.github/agents/coder.agent.md @@ -1,7 +1,6 @@ --- description: 'Implementation agent that converts detailed design specifications into working C# code following Clean Architecture principles and strict dependency rules' tools: ['execute/getTerminalOutput', 'execute/runTask', 'execute/getTaskOutput', 'execute/createAndRunTask', 'execute/runInTerminal', 'read/getNotebookSummary', 'read/readFile', 'edit/createDirectory', 'edit/createFile', 'edit/editFiles', 'edit/editNotebook', 'github/issue_read'] -model: Claude Sonnet 4.5 (copilot) required_skills: - path: '.github/skills/unit-testing/SKILL.md' when: 'mode == "unit-tests"' diff --git a/.github/agents/detailed-designer.agent.md b/.github/agents/detailed-designer.agent.md index f7b7640..f562c9c 100644 --- a/.github/agents/detailed-designer.agent.md +++ b/.github/agents/detailed-designer.agent.md @@ -1,7 +1,6 @@ --- description: 'Detailed design agent that converts approved high-level architecture into implementable specifications with explicit contracts, error handling, and test strategies' tools: ['execute/getTerminalOutput', 'execute/runTask', 'execute/getTaskOutput', 'execute/createAndRunTask', 'execute/runInTerminal', 'read/getNotebookSummary', 'read/readFile', 'edit/createDirectory', 'edit/createFile', 'edit/editFiles', 'edit/editNotebook', 'github/issue_read'] -model: Claude Sonnet 4.5 (copilot) handoffs: - label: Review Detailed Design agent: detailed-designer diff --git a/.github/skills/code-review-md-report/SKILL.md b/.github/skills/code-review-md-report/SKILL.md index b4133b7..9643c88 100644 --- a/.github/skills/code-review-md-report/SKILL.md +++ b/.github/skills/code-review-md-report/SKILL.md @@ -34,7 +34,7 @@ Use the following template **exactly** as the structure for the report file. Rep --- ```markdown -# Code Review Report +# Code Review Markdown Report | Field | Value | |-------|-------| From 5f9ad120d309cd82503c2acccfe3bdbcc5119106 Mon Sep 17 00:00:00 2001 From: Florin Coros Date: Tue, 17 Mar 2026 15:14:36 +0200 Subject: [PATCH 3/5] [AI:Coder, HUMAN:-, MODEL: Claude 3.7 Sonnet] (#1) Add CustomerOverdueOrdersData DTO, extend ICustomerService, and implement GetCustomersWithOverdueOrders --- .../Sales/CustomerOverdueOrdersData.cs | 8 +++++ Modules/Contracts/Sales/ICustomerService.cs | 2 ++ .../Sales/Sales.Services/CustomerService.cs | 32 ++++++++++++++++++- 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 Modules/Contracts/Sales/CustomerOverdueOrdersData.cs diff --git a/Modules/Contracts/Sales/CustomerOverdueOrdersData.cs b/Modules/Contracts/Sales/CustomerOverdueOrdersData.cs new file mode 100644 index 0000000..a5bfcd5 --- /dev/null +++ b/Modules/Contracts/Sales/CustomerOverdueOrdersData.cs @@ -0,0 +1,8 @@ +namespace Contracts.Sales; + +public sealed class CustomerOverdueOrdersData +{ + public required string CustomerName { get; init; } + public int OverdueOrderCount { get; init; } + public DateTime OldestOverdueOrderDate { get; init; } +} diff --git a/Modules/Contracts/Sales/ICustomerService.cs b/Modules/Contracts/Sales/ICustomerService.cs index 338392c..d345d10 100644 --- a/Modules/Contracts/Sales/ICustomerService.cs +++ b/Modules/Contracts/Sales/ICustomerService.cs @@ -7,4 +7,6 @@ public interface ICustomerService CustomerData[] GetCustomersWithOrdersStartingWith(string prefix); CustomerData[] GetCustomersWithOrdersContaining(string fragment); + + CustomerOverdueOrdersData[] GetCustomersWithOverdueOrders(); } diff --git a/Modules/Sales/Sales.Services/CustomerService.cs b/Modules/Sales/Sales.Services/CustomerService.cs index 7b49222..ab6ea83 100644 --- a/Modules/Sales/Sales.Services/CustomerService.cs +++ b/Modules/Sales/Sales.Services/CustomerService.cs @@ -3,12 +3,14 @@ using AppBoot.DependencyInjection; using Contracts.Sales; using DataAccess; +using Microsoft.Extensions.Logging; using Sales.DataModel.SalesLT; +using Sales.DataModel.Values; namespace Sales.Services; [Service(typeof(ICustomerService))] -class CustomerService(IRepository repository) : ICustomerService +class CustomerService(IRepository repository, ILogger logger) : ICustomerService { public CustomerData[] GetCustomersWithOrders() { @@ -52,4 +54,32 @@ public CustomerData[] GetCustomersWithOrdersContaining(string fragment) return GetCustomersWithOrdersFilteredBy(filter); } + + public CustomerOverdueOrdersData[] GetCustomersWithOverdueOrders() + { + logger.LogInformation("Retrieving customers with overdue orders"); + + var today = DateTime.Today; + var closedStatuses = new[] { SalesOrderHeaderStatusValues.Shipped, SalesOrderHeaderStatusValues.Cancelled }; + + var results = repository.GetEntities() + .Where(o => o.DueDate < today && !closedStatuses.Contains(o.Status)) + .GroupBy(o => o.Customer) + .Select(g => new CustomerOverdueOrdersData + { + CustomerName = !string.IsNullOrWhiteSpace(g.Key.CompanyName) + ? g.Key.CompanyName + : !string.IsNullOrWhiteSpace(g.Key.FirstName) || !string.IsNullOrWhiteSpace(g.Key.LastName) + ? $"{g.Key.FirstName} {g.Key.LastName}".Trim() + : $"Customer {g.Key.CustomerID}", + OverdueOrderCount = g.Count(), + OldestOverdueOrderDate = g.Min(o => o.DueDate) + }) + .OrderBy(c => c.OldestOverdueOrderDate) + .ToArray(); + + logger.LogDebug("Found {Count} customers with overdue orders", results.Length); + + return results; + } } From 0a3bdbd8a181344da88caf5ca3089a42054e01f7 Mon Sep 17 00:00:00 2001 From: Florin Coros Date: Tue, 17 Mar 2026 15:14:57 +0200 Subject: [PATCH 4/5] [AI:Coder, HUMAN:-, MODEL: Claude 3.7 Sonnet] (#1) Add ShowCustomersWithOverdueOrdersConsoleCommand --- ...ustomersWithOverdueOrdersConsoleCommand.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 Modules/Sales/Sales.ConsoleCommands/ShowCustomersWithOverdueOrdersConsoleCommand.cs diff --git a/Modules/Sales/Sales.ConsoleCommands/ShowCustomersWithOverdueOrdersConsoleCommand.cs b/Modules/Sales/Sales.ConsoleCommands/ShowCustomersWithOverdueOrdersConsoleCommand.cs new file mode 100644 index 0000000..8cd884f --- /dev/null +++ b/Modules/Sales/Sales.ConsoleCommands/ShowCustomersWithOverdueOrdersConsoleCommand.cs @@ -0,0 +1,29 @@ +using AppBoot.DependencyInjection; +using Contracts.ConsoleUi; +using Contracts.Sales; + +namespace Sales.ConsoleCommands; + +[Service(typeof(IConsoleCommand))] +internal sealed class ShowCustomersWithOverdueOrdersConsoleCommand(IConsole console, ICustomerService customerService) : IConsoleCommand +{ + public string MenuLabel => "Show customers with overdue orders"; + + public void Execute() + { + console.WriteLine("Retrieving customers with overdue orders..."); + + CustomerOverdueOrdersData[] customers = customerService.GetCustomersWithOverdueOrders(); + + if (customers.Length == 0) + { + console.WriteLine("No customers with overdue orders found."); + return; + } + + foreach (var c in customers) + { + console.WriteEntity(c); + } + } +} From f4e2d36d909a063438de0e83832a5a3422448f99 Mon Sep 17 00:00:00 2001 From: Florin Coros Date: Wed, 18 Mar 2026 14:42:12 +0200 Subject: [PATCH 5/5] [AI:Coder, HUMAN:-, MODEL: Claude 3.7 Sonnet] (#1) Add unit tests for GetCustomersWithOverdueOrders --- .copilot-cli/1-overdue-orders.coder.cli.md | 364 ++++++++++++++++++ AppInfraDemo.sln | 179 +++++++++ .../CustomerServiceTests.cs | 213 ++++++++++ .../Sales.Services.UnitTests.csproj | 38 ++ .../xunit.runner.json | 4 + .../Sales.Services/Sales.Services.csproj | 5 + 6 files changed, 803 insertions(+) create mode 100644 .copilot-cli/1-overdue-orders.coder.cli.md create mode 100644 Modules/Sales/Sales.Services.UnitTests/CustomerServiceTests.cs create mode 100644 Modules/Sales/Sales.Services.UnitTests/Sales.Services.UnitTests.csproj create mode 100644 Modules/Sales/Sales.Services.UnitTests/xunit.runner.json diff --git a/.copilot-cli/1-overdue-orders.coder.cli.md b/.copilot-cli/1-overdue-orders.coder.cli.md new file mode 100644 index 0000000..43e02b3 --- /dev/null +++ b/.copilot-cli/1-overdue-orders.coder.cli.md @@ -0,0 +1,364 @@ +copilot --agent=coder ` :13:08 +> -p "Implement feature #1 using high-level design from docs/workitems/1-design.md and detailed design from docs/workitems/1-detailed-design.md +> When you finish append the HANDOFF block exactly as in the format in #Completion Protocol" ` +> --allow-all-tools +I'll implement feature #1 following the design specifications. Let me start by gathering the necessary context. + +✔ Read docs\workitems\1-design.md + └ 113 lines read + +✔ Read docs\workitems\1-detailed-design.md + └ 214 lines read + +✘ github-mcp-server-issue_read + MCP error 0: failed to get issue: GET + https://api.github.com/repos/DevAgenticAI/Infra-Demo-AI/issues/1: 404 Not Found [] + +Now let me check the existing structure to understand what's already in place: + +✔ List directory Modules\Sales\Sales.Services + └ 15 files found + +✔ List directory Modules\Sales\Sales.DataModel\Values + └ 7 files found + +✔ List directory Modules\Contracts\Sales + └ 14 files found + +✔ List directory Modules\Sales\Sales.ConsoleCommands + └ 15 files found + +✔ Read Modules\Sales\Sales.Services\CustomerService.cs + └ 60 lines read + +✔ Read Modules\Contracts\Sales\ICustomerService.cs + └ 15 lines read + +✔ Read Modules\Sales\Sales.DataModel\Values\SalesOrderHeaderStatusValues.cs + └ 20 lines read + +Let me check an example console command to understand the pattern: + +✔ List directory Modules\Sales\Sales.DataModel\Generated\SalesLT + └ 16 files found + +✔ Read Modules\Sales\Sales.ConsoleCommands\CustomersWithOrdersConsoleCommand.cs + └ 34 lines read + +✔ Read Modules\Sales\Sales.DataModel\Generated\SalesLT\SalesOrderHeader.cs + └ 62 lines read + +✔ Read Modules\Sales\Sales.DataModel\Generated\SalesLT\Customer.cs + └ 52 lines read + +Perfect! Now I have all the context. Let me create a plan: + +``` +Mode: IMPLEMENT (Simple - 3 components) +Issue #1 — Show customers with overdue orders +Files: +- Modules/Contracts/Sales/CustomerOverdueOrdersData.cs (new) +- Modules/Contracts/Sales/ICustomerService.cs (extend) +- Modules/Sales/Sales.Services/CustomerService.cs (extend) +- Modules/Sales/Sales.ConsoleCommands/ShowCustomersWithOverdueOrdersConsoleCommand.cs (new) +Commits: +1. "Add CustomerOverdueOrdersData DTO and extend ICustomerService interface" +2. "Implement GetCustomersWithOverdueOrders in CustomerService" +3. "Add ShowCustomersWithOverdueOrdersConsoleCommand" +Dependency check: ✅ Sales module only, IRepository for read-only, no cross-module refs +``` + +Let's implement Phase 1: Contracts & DTOs + +✔ Create Modules\Contracts\Sales\CustomerOverdueOrdersData.cs (+9) + +✔ Edit Modules\Contracts\Sales\ICustomerService.cs (+2) + ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ + │ 7      CustomerData[] GetCustomersWithOrdersStartingWith(string prefix); │ + │ 8      │ + │ 9      CustomerData[] GetCustomersWithOrdersContaining(string fragment); │ + │ 10  +   │ + │ 11  +   CustomerOverdueOrdersData[] GetCustomersWithOverdueOrders(); │ + │ 12     } │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ + +✔ $ dotnet build AppInfraDemo.sln --no-incremental + └ 98 lines... + +Expected build error - need to implement the method. Now implementing Phase 2: Service Implementation + +✔ Edit Modules\Sales\Sales.Services\CustomerService.cs (+2) + ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ + │ 3     using AppBoot.DependencyInjection; │ + │ 4     using Contracts.Sales; │ + │ 5     using DataAccess; │ + │ 6  +  using Microsoft.Extensions.Logging; │ + │ 7     using Sales.DataModel.SalesLT; │ + │ 8  +  using Sales.DataModel.Values; │ + │ 9      │ + │ 10     namespace Sales.Services; │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ + +✔ Edit Modules\Sales\Sales.Services\CustomerService.cs (+1 -1) + ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ + │ 10     namespace Sales.Services; │ + │ 11      │ + │ 12     [Service(typeof(ICustomerService))] │ + │ 13  -  class CustomerService(IRepository repository) : ICustomerService │ + │ 13  +  class CustomerService(IRepository repository, ILogger logger) : │ + │ ICustomerService │ + │ 14     { │ + │ 15      public CustomerData[] GetCustomersWithOrders() │ + │ 16      { │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ + +✔ Edit Modules\Sales\Sales.Services\CustomerService.cs (+28) + ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ + │ 54      │ + │ 55      return GetCustomersWithOrdersFilteredBy(filter); │ + │ 56      } │ + │ 57  +   │ + │ 58  +   public CustomerOverdueOrdersData[] GetCustomersWithOverdueOrders() │ + │ 59  +   { │ + │ 60  +   logger.LogInformation("Retrieving customers with overdue orders"); │ + │ 61  +   │ + │ 62  +   var today = DateTime.Today; │ + │ 63  +   var closedStatuses = new[] { SalesOrderHeaderStatusValues.Shipped, │ + │ SalesOrderHeaderStatusValues.Cancelled }; │ + │ 64  +   │ + │ 65  +   var results = repository.GetEntities() │ + │ 66  +   .Where(o => o.DueDate < today && !closedStatuses.Contains(o.Status)) │ + │ 67  +   .GroupBy(o => o.Customer) │ + │ 68  +   .Select(g => new CustomerOverdueOrdersData │ + │ 69  +   { │ + │ 70  +   CustomerName = !string.IsNullOrWhiteSpace(g.Key.CompanyName) │ + │ 71  +   ? g.Key.CompanyName │ + │ 72  +   : !string.IsNullOrWhiteSpace(g.Key.FirstName) || │ + │ !string.IsNullOrWhiteSpace(g.Key.LastName) │ + │ 73  +   ? $"{g.Key.FirstName} {g.Key.LastName}".Trim() │ + │ 74  +   : $"Customer {g.Key.CustomerID}", │ + │ 75  +   OverdueOrderCount = g.Count(), │ + │ 76  +   OldestOverdueOrderDate = g.Min(o => o.DueDate) │ + │ 77  +   }) │ + │ 78  +   .OrderBy(c => c.OldestOverdueOrderDate) │ + │ 79  +   .ToArray(); │ + │ 80  +   │ + │ 81  +   logger.LogDebug("Found {Count} customers with overdue orders", │ + │ results.Length); │ + │ 82  +   │ + │ 83  +   return results; │ + │ 84  +   } │ + │ 85     } │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ + +✔ $ dotnet build AppInfraDemo.sln --no-incremental + └ 116 lines... + +Great! Build succeeded. Now let's commit phases 1 & 2 together and create the console command: + +✔ $ git add Modules/Contracts/Sales/CustomerOverdueOrdersData.cs + Modules/Contracts/Sales/ICustomerService.cs Modules/Sales/Sales.Services/CustomerService.cs && + git commit -m "[AI:Coder, HUMAN:-, MODEL: Claude 3.7 Sonnet] (#1) Add CustomerOverdueOrdersData + DTO, extend ICustomerService, and implement GetCustomersWithOverdueOrders" + └ 5 lines... + +Now implementing Phase 3: Console Command + +✔ Create Modules\Sales\Sales.ConsoleCommands\ShowCustomersWithOverdueOrdersConsoleCommand.cs (+30) + +✔ $ dotnet build AppInfraDemo.sln --no-incremental && dotnet test AppInfraDemo.sln --no-build + └ 177 lines... + +Perfect! Build and tests passed. Now let's commit the console command: + +✔ $ git add Modules/Sales/Sales.ConsoleCommands/ShowCustomersWithOverdueOrdersConsoleCommand.cs && + git commit -m "[AI:Coder, HUMAN:-, MODEL: Claude 3.7 Sonnet] (#1) Add + ShowCustomersWithOverdueOrdersConsoleCommand" + └ 5 lines... + +Now let's get the commit information for the handoff: + +✔ $ git --no-pager log -2 --oneline + └ 5 lines... + +Excellent! Implementation complete. All phases completed successfully: +- Phase 1 & 2: DTO, interface extension, and service implementation committed +- Phase 3: Console command committed +- Build: 0 errors, 57 warnings (pre-existing, unrelated to feature) +- Tests: All 3 tests passed + +``` +HANDOFF_START +issue-id #1 +issue-description: Show customers with overdue orders +implementation-mode: IMPLEMENT (Simple - 3 components) +file-list: Modules/Contracts/Sales/CustomerOverdueOrdersData.cs, Modules/Contracts/Sales/ICustomerService.cs, Modules/Sales/Sales.Services/CustomerService.cs, Modules/Sales/Sales.ConsoleCommands/ShowCustomersWithOverdueOrdersConsoleCommand.cs +build-status: PASS (0 errors, 57 warnings - all pre-existing) +build-iterations: 2 +test-status: PASS (3/3 passed) +test-iterations: 1 +design-deviations: NONE +commits: "03866e1: [AI:Coder, HUMAN:-, MODEL: Claude 3.7 Sonnet] (#1) Add CustomerOverdueOrdersData DTO, extend ICustomerService, and implement GetCustomersWithOverdueOrders, 9754ccb: [AI:Coder, HUMAN:-, MODEL: Claude 3.7 Sonnet] (#1) Add ShowCustomersWithOverdueOrdersConsoleCommand" +next-steps: Feature ready for testing - verify console command displays customers with overdue orders correctly, showing customer name, count of overdue orders, and oldest overdue date sorted by oldest date ascending +handoff-to: HUMAN +HANDOFF_END +``` + + +Total usage est: 1 Premium request +Total duration (API): 1m 17.302s +Total duration (wall): 1m 47.487s +Total code changes: 72 lines added, 1 lines removed +Usage by model: + claude-sonnet-4.5 337.2k input, 4.9k output, 298.5k cache read (Est. 1 Premium request) + +--- + + florin@Florin_Razer  C:    cli  feature-14  feature/cli/14-overdue-orders  copilot --agent=coder ` +> -p "Use Mode 2 to create Unit test for feature #1 as specified into the detailed design from docs/workitems/1-detailed-design.md +> Context: Mode=2; Files=Modules/Contracts/Sales/CustomerOverdueOrdersData.cs, Modules/Contracts/Sales/ICustomerService.cs, Modules/Sales/Sales.Services/CustomerService.cs, Modules/Sales/Sales.ConsoleCommands/ShowCustomersWithOverdueOrdersConsoleCommand.cs; Build=PASS; Tests=PASS; Deviations=NONE Commits=03866e1: [AI:Coder, HUMAN:-, MODEL: Claude 3.7 Sonnet] (#1) Add CustomerOverdueOrdersData DTO, extend ICustomerService, and implement GetCustomersWithOverdueOrders, 9754ccb: [AI:Coder, HUMAN:-, MODEL: Claude 3.7 Sonnet] (#1) Add ShowCustomersWithOverdueOrdersConsoleCommand" ` +> --allow-all-tools +I'll create unit tests for feature #1 following Mode 2. Let me first gather the context. + +✔ List directory .github\skills\unit-testing + └ 8 files found + +✘ github-mcp-server-issue_read + MCP error 0: failed to get issue: GET + https://api.github.com/repos/DevAgenticAI/Infra-Demo-AI/issues/1: 404 Not Found [] + +✔ Read Modules\Sales\Sales.Services\CustomerService.cs + └ 90 lines read + +✔ Read docs\workitems\1-detailed-design.md + └ 214 lines read + +✔ Read .github\skills\unit-testing\SKILL.md + └ 483 lines read + +✔ List directory Modules\Sales + └ 10 files found + +✘ Read Modules\Sales\Sales.Services.UnitTests + Path does not exist + +``` +Mode: UNIT TESTS +Issue #1 — Show customers with overdue orders +Files: Modules/Sales/Sales.Services.UnitTests/CustomerServiceTests.cs (create project + tests) +Commit: "[AI:Coder, HUMAN:-, MODEL: Claude 3.7 Sonnet] (#1) Add unit tests for GetCustomersWithOverdueOrders" +Dependency check: ✅ Test project refs Sales.Services, Contracts, DataAccess only +``` + +✔ List directory Modules\Sales\Sales.DataModel\Generated\SalesLT + └ 16 files found + +✔ Read .github\skills\unit-testing\CHEATSHEET.md + └ 322 lines read + +✔ Read Modules\Sales\Sales.DataModel\Values\SalesOrderHeaderStatusValues.cs + └ 20 lines read + +✔ Read Modules\Sales\Sales.DataModel\Generated\SalesLT\Customer.cs:1-40 + └ 44 lines read + +✔ Read Modules\Sales\Sales.DataModel\Generated\SalesLT\SalesOrderHeader.cs:1-40 + └ 45 lines read + +Now I'll create the test project and unit tests. + +✔ $ New-Item -ItemType Directory -Path "Modules\Sales\Sales.Services.UnitTests" -Force + └ 6 lines... + +✔ Create Modules\Sales\Sales.Services.UnitTests\xunit.runner.json (+5) + +✔ Create Modules\Sales\Sales.Services.UnitTests\Sales.Services.UnitTests.csproj (+39) + +✔ Create Modules\Sales\Sales.Services.UnitTests\CustomerServiceTests.cs (+214) + +✔ $ dotnet sln add Modules\Sales\Sales.Services.UnitTests\Sales.Services.UnitTests.csproj + └ 3 lines... + +✔ $ dotnet sln AppInfraDemo.sln add + Modules\Sales\Sales.Services.UnitTests\Sales.Services.UnitTests.csproj + └ 2 lines... + +✔ $ dotnet build AppInfraDemo.sln + └ 108 lines... + +`CustomerService` is `internal`, I need to use `InternalsVisibleTo` attribute. + +✔ Read Modules\Sales\Sales.Services\Sales.Services.csproj + └ 22 lines read + +✔ Edit Modules\Sales\Sales.Services\Sales.Services.csproj (+4) + ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ + │ 14      │ + │ 15      │ + │ 16      │ + │ 17  +   │ + │ 18  +   │ + │ 19  +   │ + │ 20  +   │ + │ 21      │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ + +✔ $ dotnet build AppInfraDemo.sln + └ 112 lines... + +✔ $ dotnet test AppInfraDemo.sln --no-build + └ 104 lines... + +NSubstitute can't mock `ILogger` with a strong-named assembly. I need to add InternalsVisibleTo for DynamicProxyGenAssembly2 to the Sales.Services project. + +✔ Edit Modules\Sales\Sales.Services\Sales.Services.csproj (+1) + ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ + │ 16      │ + │ 17      │ + │ 18      │ + │ 19  +   │ + │ 20      │ + │ 21      │ + │ 22      │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ + +✔ $ dotnet build AppInfraDemo.sln && dotnet test AppInfraDemo.sln --no-build + └ 103 lines... + +Perfect! All tests pass. Now let me commit the changes. + +✔ $ git add -A && git commit -m "[AI:Coder, HUMAN:-, MODEL: Claude 3.7 Sonnet] (#1) Add unit tests + for GetCustomersWithOverdueOrders" + └ 7 lines... + +✔ $ git log --oneline -3 + └ 6 lines... + +``` +HANDOFF_START +issue-id #1 +issue-description: Show customers with overdue orders +implementation-mode: UNIT TESTS (Simple) +file-list: Modules/Sales/Sales.Services.UnitTests/Sales.Services.UnitTests.csproj, Modules/Sales/Sales.Services.UnitTests/xunit.runner.json, Modules/Sales/Sales.Services.UnitTests/CustomerServiceTests.cs, Modules/Sales/Sales.Services/Sales.Services.csproj, AppInfraDemo.sln +build-status: PASS (0 errors, 4 warnings) +build-iterations: 3 +test-status: PASS (11/11 passed) +test-iterations: 2 +design-deviations: NONE +commits: "3e07d1a: [AI:Coder, HUMAN:-, MODEL: Claude 3.7 Sonnet] (#1) Add unit tests for GetCustomersWithOverdueOrders" +next-steps: All unit tests implemented and passing. Feature complete with comprehensive test coverage for GetCustomersWithOverdueOrders including edge cases for filtering, sorting, and aggregation. +handoff-to: HUMAN +HANDOFF_END +``` + + +Total usage est: 1 Premium request +Total duration (API): 2m 19.945s +Total duration (wall): 3m 6.396s +Total code changes: 263 lines added, 0 lines removed +Usage by model: + claude-sonnet-4.5 643.0k input, 7.1k output, 591.4k cache read (Est. 1 Premium request) \ No newline at end of file diff --git a/AppInfraDemo.sln b/AppInfraDemo.sln index 6732c11..b605ec1 100644 --- a/AppInfraDemo.sln +++ b/AppInfraDemo.sln @@ -86,92 +86,270 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PersonsManagement.DataModel EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PersonsManagement.ConsoleCommands", "Modules\PersonsManagement\PersonsManagement.ConsoleCommands\PersonsManagement.ConsoleCommands.csproj", "{EA568308-EFF1-BB6F-8BA8-0DFE8AB9F45D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sales.Services.UnitTests", "Modules\Sales\Sales.Services.UnitTests\Sales.Services.UnitTests.csproj", "{CD38901C-17D2-4DA6-A61D-96AFE0B432E9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {E513E557-3FAF-0D2E-F72A-11171988EE60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E513E557-3FAF-0D2E-F72A-11171988EE60}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E513E557-3FAF-0D2E-F72A-11171988EE60}.Debug|x64.ActiveCfg = Debug|Any CPU + {E513E557-3FAF-0D2E-F72A-11171988EE60}.Debug|x64.Build.0 = Debug|Any CPU + {E513E557-3FAF-0D2E-F72A-11171988EE60}.Debug|x86.ActiveCfg = Debug|Any CPU + {E513E557-3FAF-0D2E-F72A-11171988EE60}.Debug|x86.Build.0 = Debug|Any CPU {E513E557-3FAF-0D2E-F72A-11171988EE60}.Release|Any CPU.ActiveCfg = Release|Any CPU {E513E557-3FAF-0D2E-F72A-11171988EE60}.Release|Any CPU.Build.0 = Release|Any CPU + {E513E557-3FAF-0D2E-F72A-11171988EE60}.Release|x64.ActiveCfg = Release|Any CPU + {E513E557-3FAF-0D2E-F72A-11171988EE60}.Release|x64.Build.0 = Release|Any CPU + {E513E557-3FAF-0D2E-F72A-11171988EE60}.Release|x86.ActiveCfg = Release|Any CPU + {E513E557-3FAF-0D2E-F72A-11171988EE60}.Release|x86.Build.0 = Release|Any CPU {991B4805-BB14-3351-1D86-58C4538A460A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {991B4805-BB14-3351-1D86-58C4538A460A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {991B4805-BB14-3351-1D86-58C4538A460A}.Debug|x64.ActiveCfg = Debug|Any CPU + {991B4805-BB14-3351-1D86-58C4538A460A}.Debug|x64.Build.0 = Debug|Any CPU + {991B4805-BB14-3351-1D86-58C4538A460A}.Debug|x86.ActiveCfg = Debug|Any CPU + {991B4805-BB14-3351-1D86-58C4538A460A}.Debug|x86.Build.0 = Debug|Any CPU {991B4805-BB14-3351-1D86-58C4538A460A}.Release|Any CPU.ActiveCfg = Release|Any CPU {991B4805-BB14-3351-1D86-58C4538A460A}.Release|Any CPU.Build.0 = Release|Any CPU + {991B4805-BB14-3351-1D86-58C4538A460A}.Release|x64.ActiveCfg = Release|Any CPU + {991B4805-BB14-3351-1D86-58C4538A460A}.Release|x64.Build.0 = Release|Any CPU + {991B4805-BB14-3351-1D86-58C4538A460A}.Release|x86.ActiveCfg = Release|Any CPU + {991B4805-BB14-3351-1D86-58C4538A460A}.Release|x86.Build.0 = Release|Any CPU {B5FA3BF8-3AEC-81FC-BC59-053550300A2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B5FA3BF8-3AEC-81FC-BC59-053550300A2F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B5FA3BF8-3AEC-81FC-BC59-053550300A2F}.Debug|x64.ActiveCfg = Debug|Any CPU + {B5FA3BF8-3AEC-81FC-BC59-053550300A2F}.Debug|x64.Build.0 = Debug|Any CPU + {B5FA3BF8-3AEC-81FC-BC59-053550300A2F}.Debug|x86.ActiveCfg = Debug|Any CPU + {B5FA3BF8-3AEC-81FC-BC59-053550300A2F}.Debug|x86.Build.0 = Debug|Any CPU {B5FA3BF8-3AEC-81FC-BC59-053550300A2F}.Release|Any CPU.ActiveCfg = Release|Any CPU {B5FA3BF8-3AEC-81FC-BC59-053550300A2F}.Release|Any CPU.Build.0 = Release|Any CPU + {B5FA3BF8-3AEC-81FC-BC59-053550300A2F}.Release|x64.ActiveCfg = Release|Any CPU + {B5FA3BF8-3AEC-81FC-BC59-053550300A2F}.Release|x64.Build.0 = Release|Any CPU + {B5FA3BF8-3AEC-81FC-BC59-053550300A2F}.Release|x86.ActiveCfg = Release|Any CPU + {B5FA3BF8-3AEC-81FC-BC59-053550300A2F}.Release|x86.Build.0 = Release|Any CPU {785DA49E-457F-734B-E08E-4F5C56B0432A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {785DA49E-457F-734B-E08E-4F5C56B0432A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {785DA49E-457F-734B-E08E-4F5C56B0432A}.Debug|x64.ActiveCfg = Debug|Any CPU + {785DA49E-457F-734B-E08E-4F5C56B0432A}.Debug|x64.Build.0 = Debug|Any CPU + {785DA49E-457F-734B-E08E-4F5C56B0432A}.Debug|x86.ActiveCfg = Debug|Any CPU + {785DA49E-457F-734B-E08E-4F5C56B0432A}.Debug|x86.Build.0 = Debug|Any CPU {785DA49E-457F-734B-E08E-4F5C56B0432A}.Release|Any CPU.ActiveCfg = Release|Any CPU {785DA49E-457F-734B-E08E-4F5C56B0432A}.Release|Any CPU.Build.0 = Release|Any CPU + {785DA49E-457F-734B-E08E-4F5C56B0432A}.Release|x64.ActiveCfg = Release|Any CPU + {785DA49E-457F-734B-E08E-4F5C56B0432A}.Release|x64.Build.0 = Release|Any CPU + {785DA49E-457F-734B-E08E-4F5C56B0432A}.Release|x86.ActiveCfg = Release|Any CPU + {785DA49E-457F-734B-E08E-4F5C56B0432A}.Release|x86.Build.0 = Release|Any CPU {3DA49FD3-1336-6C92-F503-0527892DF3C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3DA49FD3-1336-6C92-F503-0527892DF3C6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3DA49FD3-1336-6C92-F503-0527892DF3C6}.Debug|x64.ActiveCfg = Debug|Any CPU + {3DA49FD3-1336-6C92-F503-0527892DF3C6}.Debug|x64.Build.0 = Debug|Any CPU + {3DA49FD3-1336-6C92-F503-0527892DF3C6}.Debug|x86.ActiveCfg = Debug|Any CPU + {3DA49FD3-1336-6C92-F503-0527892DF3C6}.Debug|x86.Build.0 = Debug|Any CPU {3DA49FD3-1336-6C92-F503-0527892DF3C6}.Release|Any CPU.ActiveCfg = Release|Any CPU {3DA49FD3-1336-6C92-F503-0527892DF3C6}.Release|Any CPU.Build.0 = Release|Any CPU + {3DA49FD3-1336-6C92-F503-0527892DF3C6}.Release|x64.ActiveCfg = Release|Any CPU + {3DA49FD3-1336-6C92-F503-0527892DF3C6}.Release|x64.Build.0 = Release|Any CPU + {3DA49FD3-1336-6C92-F503-0527892DF3C6}.Release|x86.ActiveCfg = Release|Any CPU + {3DA49FD3-1336-6C92-F503-0527892DF3C6}.Release|x86.Build.0 = Release|Any CPU {4A7B5195-15A6-1849-BF6A-C81E6F07C352}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4A7B5195-15A6-1849-BF6A-C81E6F07C352}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4A7B5195-15A6-1849-BF6A-C81E6F07C352}.Debug|x64.ActiveCfg = Debug|Any CPU + {4A7B5195-15A6-1849-BF6A-C81E6F07C352}.Debug|x64.Build.0 = Debug|Any CPU + {4A7B5195-15A6-1849-BF6A-C81E6F07C352}.Debug|x86.ActiveCfg = Debug|Any CPU + {4A7B5195-15A6-1849-BF6A-C81E6F07C352}.Debug|x86.Build.0 = Debug|Any CPU {4A7B5195-15A6-1849-BF6A-C81E6F07C352}.Release|Any CPU.ActiveCfg = Release|Any CPU {4A7B5195-15A6-1849-BF6A-C81E6F07C352}.Release|Any CPU.Build.0 = Release|Any CPU + {4A7B5195-15A6-1849-BF6A-C81E6F07C352}.Release|x64.ActiveCfg = Release|Any CPU + {4A7B5195-15A6-1849-BF6A-C81E6F07C352}.Release|x64.Build.0 = Release|Any CPU + {4A7B5195-15A6-1849-BF6A-C81E6F07C352}.Release|x86.ActiveCfg = Release|Any CPU + {4A7B5195-15A6-1849-BF6A-C81E6F07C352}.Release|x86.Build.0 = Release|Any CPU {85B66B0C-DDB4-E33A-8626-891A7892B178}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {85B66B0C-DDB4-E33A-8626-891A7892B178}.Debug|Any CPU.Build.0 = Debug|Any CPU + {85B66B0C-DDB4-E33A-8626-891A7892B178}.Debug|x64.ActiveCfg = Debug|Any CPU + {85B66B0C-DDB4-E33A-8626-891A7892B178}.Debug|x64.Build.0 = Debug|Any CPU + {85B66B0C-DDB4-E33A-8626-891A7892B178}.Debug|x86.ActiveCfg = Debug|Any CPU + {85B66B0C-DDB4-E33A-8626-891A7892B178}.Debug|x86.Build.0 = Debug|Any CPU {85B66B0C-DDB4-E33A-8626-891A7892B178}.Release|Any CPU.ActiveCfg = Release|Any CPU {85B66B0C-DDB4-E33A-8626-891A7892B178}.Release|Any CPU.Build.0 = Release|Any CPU + {85B66B0C-DDB4-E33A-8626-891A7892B178}.Release|x64.ActiveCfg = Release|Any CPU + {85B66B0C-DDB4-E33A-8626-891A7892B178}.Release|x64.Build.0 = Release|Any CPU + {85B66B0C-DDB4-E33A-8626-891A7892B178}.Release|x86.ActiveCfg = Release|Any CPU + {85B66B0C-DDB4-E33A-8626-891A7892B178}.Release|x86.Build.0 = Release|Any CPU {4A29280D-4BB5-4C22-EA77-E2FFB887BE9A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4A29280D-4BB5-4C22-EA77-E2FFB887BE9A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4A29280D-4BB5-4C22-EA77-E2FFB887BE9A}.Debug|x64.ActiveCfg = Debug|Any CPU + {4A29280D-4BB5-4C22-EA77-E2FFB887BE9A}.Debug|x64.Build.0 = Debug|Any CPU + {4A29280D-4BB5-4C22-EA77-E2FFB887BE9A}.Debug|x86.ActiveCfg = Debug|Any CPU + {4A29280D-4BB5-4C22-EA77-E2FFB887BE9A}.Debug|x86.Build.0 = Debug|Any CPU {4A29280D-4BB5-4C22-EA77-E2FFB887BE9A}.Release|Any CPU.ActiveCfg = Release|Any CPU {4A29280D-4BB5-4C22-EA77-E2FFB887BE9A}.Release|Any CPU.Build.0 = Release|Any CPU + {4A29280D-4BB5-4C22-EA77-E2FFB887BE9A}.Release|x64.ActiveCfg = Release|Any CPU + {4A29280D-4BB5-4C22-EA77-E2FFB887BE9A}.Release|x64.Build.0 = Release|Any CPU + {4A29280D-4BB5-4C22-EA77-E2FFB887BE9A}.Release|x86.ActiveCfg = Release|Any CPU + {4A29280D-4BB5-4C22-EA77-E2FFB887BE9A}.Release|x86.Build.0 = Release|Any CPU {8ECFDB0C-9146-4C51-B8AF-3DC696492DAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8ECFDB0C-9146-4C51-B8AF-3DC696492DAE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8ECFDB0C-9146-4C51-B8AF-3DC696492DAE}.Debug|x64.ActiveCfg = Debug|Any CPU + {8ECFDB0C-9146-4C51-B8AF-3DC696492DAE}.Debug|x64.Build.0 = Debug|Any CPU + {8ECFDB0C-9146-4C51-B8AF-3DC696492DAE}.Debug|x86.ActiveCfg = Debug|Any CPU + {8ECFDB0C-9146-4C51-B8AF-3DC696492DAE}.Debug|x86.Build.0 = Debug|Any CPU {8ECFDB0C-9146-4C51-B8AF-3DC696492DAE}.Release|Any CPU.ActiveCfg = Release|Any CPU {8ECFDB0C-9146-4C51-B8AF-3DC696492DAE}.Release|Any CPU.Build.0 = Release|Any CPU + {8ECFDB0C-9146-4C51-B8AF-3DC696492DAE}.Release|x64.ActiveCfg = Release|Any CPU + {8ECFDB0C-9146-4C51-B8AF-3DC696492DAE}.Release|x64.Build.0 = Release|Any CPU + {8ECFDB0C-9146-4C51-B8AF-3DC696492DAE}.Release|x86.ActiveCfg = Release|Any CPU + {8ECFDB0C-9146-4C51-B8AF-3DC696492DAE}.Release|x86.Build.0 = Release|Any CPU {2DE05342-1527-0EBA-24EC-A9209FB057BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2DE05342-1527-0EBA-24EC-A9209FB057BA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2DE05342-1527-0EBA-24EC-A9209FB057BA}.Debug|x64.ActiveCfg = Debug|Any CPU + {2DE05342-1527-0EBA-24EC-A9209FB057BA}.Debug|x64.Build.0 = Debug|Any CPU + {2DE05342-1527-0EBA-24EC-A9209FB057BA}.Debug|x86.ActiveCfg = Debug|Any CPU + {2DE05342-1527-0EBA-24EC-A9209FB057BA}.Debug|x86.Build.0 = Debug|Any CPU {2DE05342-1527-0EBA-24EC-A9209FB057BA}.Release|Any CPU.ActiveCfg = Release|Any CPU {2DE05342-1527-0EBA-24EC-A9209FB057BA}.Release|Any CPU.Build.0 = Release|Any CPU + {2DE05342-1527-0EBA-24EC-A9209FB057BA}.Release|x64.ActiveCfg = Release|Any CPU + {2DE05342-1527-0EBA-24EC-A9209FB057BA}.Release|x64.Build.0 = Release|Any CPU + {2DE05342-1527-0EBA-24EC-A9209FB057BA}.Release|x86.ActiveCfg = Release|Any CPU + {2DE05342-1527-0EBA-24EC-A9209FB057BA}.Release|x86.Build.0 = Release|Any CPU {F7FCFFBE-664B-53DA-4E09-09D64789573C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F7FCFFBE-664B-53DA-4E09-09D64789573C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F7FCFFBE-664B-53DA-4E09-09D64789573C}.Debug|x64.ActiveCfg = Debug|Any CPU + {F7FCFFBE-664B-53DA-4E09-09D64789573C}.Debug|x64.Build.0 = Debug|Any CPU + {F7FCFFBE-664B-53DA-4E09-09D64789573C}.Debug|x86.ActiveCfg = Debug|Any CPU + {F7FCFFBE-664B-53DA-4E09-09D64789573C}.Debug|x86.Build.0 = Debug|Any CPU {F7FCFFBE-664B-53DA-4E09-09D64789573C}.Release|Any CPU.ActiveCfg = Release|Any CPU {F7FCFFBE-664B-53DA-4E09-09D64789573C}.Release|Any CPU.Build.0 = Release|Any CPU + {F7FCFFBE-664B-53DA-4E09-09D64789573C}.Release|x64.ActiveCfg = Release|Any CPU + {F7FCFFBE-664B-53DA-4E09-09D64789573C}.Release|x64.Build.0 = Release|Any CPU + {F7FCFFBE-664B-53DA-4E09-09D64789573C}.Release|x86.ActiveCfg = Release|Any CPU + {F7FCFFBE-664B-53DA-4E09-09D64789573C}.Release|x86.Build.0 = Release|Any CPU {200D652A-807B-44F8-7919-EC5345C67714}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {200D652A-807B-44F8-7919-EC5345C67714}.Debug|Any CPU.Build.0 = Debug|Any CPU + {200D652A-807B-44F8-7919-EC5345C67714}.Debug|x64.ActiveCfg = Debug|Any CPU + {200D652A-807B-44F8-7919-EC5345C67714}.Debug|x64.Build.0 = Debug|Any CPU + {200D652A-807B-44F8-7919-EC5345C67714}.Debug|x86.ActiveCfg = Debug|Any CPU + {200D652A-807B-44F8-7919-EC5345C67714}.Debug|x86.Build.0 = Debug|Any CPU {200D652A-807B-44F8-7919-EC5345C67714}.Release|Any CPU.ActiveCfg = Release|Any CPU {200D652A-807B-44F8-7919-EC5345C67714}.Release|Any CPU.Build.0 = Release|Any CPU + {200D652A-807B-44F8-7919-EC5345C67714}.Release|x64.ActiveCfg = Release|Any CPU + {200D652A-807B-44F8-7919-EC5345C67714}.Release|x64.Build.0 = Release|Any CPU + {200D652A-807B-44F8-7919-EC5345C67714}.Release|x86.ActiveCfg = Release|Any CPU + {200D652A-807B-44F8-7919-EC5345C67714}.Release|x86.Build.0 = Release|Any CPU {197C2EA8-58EA-A768-C664-F7AA13EDC7ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {197C2EA8-58EA-A768-C664-F7AA13EDC7ED}.Debug|Any CPU.Build.0 = Debug|Any CPU + {197C2EA8-58EA-A768-C664-F7AA13EDC7ED}.Debug|x64.ActiveCfg = Debug|Any CPU + {197C2EA8-58EA-A768-C664-F7AA13EDC7ED}.Debug|x64.Build.0 = Debug|Any CPU + {197C2EA8-58EA-A768-C664-F7AA13EDC7ED}.Debug|x86.ActiveCfg = Debug|Any CPU + {197C2EA8-58EA-A768-C664-F7AA13EDC7ED}.Debug|x86.Build.0 = Debug|Any CPU {197C2EA8-58EA-A768-C664-F7AA13EDC7ED}.Release|Any CPU.ActiveCfg = Release|Any CPU {197C2EA8-58EA-A768-C664-F7AA13EDC7ED}.Release|Any CPU.Build.0 = Release|Any CPU + {197C2EA8-58EA-A768-C664-F7AA13EDC7ED}.Release|x64.ActiveCfg = Release|Any CPU + {197C2EA8-58EA-A768-C664-F7AA13EDC7ED}.Release|x64.Build.0 = Release|Any CPU + {197C2EA8-58EA-A768-C664-F7AA13EDC7ED}.Release|x86.ActiveCfg = Release|Any CPU + {197C2EA8-58EA-A768-C664-F7AA13EDC7ED}.Release|x86.Build.0 = Release|Any CPU {3DC25EC8-43A5-D9F7-A6C7-346B14E905A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3DC25EC8-43A5-D9F7-A6C7-346B14E905A2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3DC25EC8-43A5-D9F7-A6C7-346B14E905A2}.Debug|x64.ActiveCfg = Debug|Any CPU + {3DC25EC8-43A5-D9F7-A6C7-346B14E905A2}.Debug|x64.Build.0 = Debug|Any CPU + {3DC25EC8-43A5-D9F7-A6C7-346B14E905A2}.Debug|x86.ActiveCfg = Debug|Any CPU + {3DC25EC8-43A5-D9F7-A6C7-346B14E905A2}.Debug|x86.Build.0 = Debug|Any CPU {3DC25EC8-43A5-D9F7-A6C7-346B14E905A2}.Release|Any CPU.ActiveCfg = Release|Any CPU {3DC25EC8-43A5-D9F7-A6C7-346B14E905A2}.Release|Any CPU.Build.0 = Release|Any CPU + {3DC25EC8-43A5-D9F7-A6C7-346B14E905A2}.Release|x64.ActiveCfg = Release|Any CPU + {3DC25EC8-43A5-D9F7-A6C7-346B14E905A2}.Release|x64.Build.0 = Release|Any CPU + {3DC25EC8-43A5-D9F7-A6C7-346B14E905A2}.Release|x86.ActiveCfg = Release|Any CPU + {3DC25EC8-43A5-D9F7-A6C7-346B14E905A2}.Release|x86.Build.0 = Release|Any CPU {5D518014-2AD1-45DA-A3C3-BBB8DA215E15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5D518014-2AD1-45DA-A3C3-BBB8DA215E15}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5D518014-2AD1-45DA-A3C3-BBB8DA215E15}.Debug|x64.ActiveCfg = Debug|Any CPU + {5D518014-2AD1-45DA-A3C3-BBB8DA215E15}.Debug|x64.Build.0 = Debug|Any CPU + {5D518014-2AD1-45DA-A3C3-BBB8DA215E15}.Debug|x86.ActiveCfg = Debug|Any CPU + {5D518014-2AD1-45DA-A3C3-BBB8DA215E15}.Debug|x86.Build.0 = Debug|Any CPU {5D518014-2AD1-45DA-A3C3-BBB8DA215E15}.Release|Any CPU.ActiveCfg = Release|Any CPU {5D518014-2AD1-45DA-A3C3-BBB8DA215E15}.Release|Any CPU.Build.0 = Release|Any CPU + {5D518014-2AD1-45DA-A3C3-BBB8DA215E15}.Release|x64.ActiveCfg = Release|Any CPU + {5D518014-2AD1-45DA-A3C3-BBB8DA215E15}.Release|x64.Build.0 = Release|Any CPU + {5D518014-2AD1-45DA-A3C3-BBB8DA215E15}.Release|x86.ActiveCfg = Release|Any CPU + {5D518014-2AD1-45DA-A3C3-BBB8DA215E15}.Release|x86.Build.0 = Release|Any CPU {23D05CB3-970B-7AF0-4EBA-6E5705B0A970}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {23D05CB3-970B-7AF0-4EBA-6E5705B0A970}.Debug|Any CPU.Build.0 = Debug|Any CPU + {23D05CB3-970B-7AF0-4EBA-6E5705B0A970}.Debug|x64.ActiveCfg = Debug|Any CPU + {23D05CB3-970B-7AF0-4EBA-6E5705B0A970}.Debug|x64.Build.0 = Debug|Any CPU + {23D05CB3-970B-7AF0-4EBA-6E5705B0A970}.Debug|x86.ActiveCfg = Debug|Any CPU + {23D05CB3-970B-7AF0-4EBA-6E5705B0A970}.Debug|x86.Build.0 = Debug|Any CPU {23D05CB3-970B-7AF0-4EBA-6E5705B0A970}.Release|Any CPU.ActiveCfg = Release|Any CPU {23D05CB3-970B-7AF0-4EBA-6E5705B0A970}.Release|Any CPU.Build.0 = Release|Any CPU + {23D05CB3-970B-7AF0-4EBA-6E5705B0A970}.Release|x64.ActiveCfg = Release|Any CPU + {23D05CB3-970B-7AF0-4EBA-6E5705B0A970}.Release|x64.Build.0 = Release|Any CPU + {23D05CB3-970B-7AF0-4EBA-6E5705B0A970}.Release|x86.ActiveCfg = Release|Any CPU + {23D05CB3-970B-7AF0-4EBA-6E5705B0A970}.Release|x86.Build.0 = Release|Any CPU {8B7674B2-8E1E-1179-9EA1-B37CDB1B0CCF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8B7674B2-8E1E-1179-9EA1-B37CDB1B0CCF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8B7674B2-8E1E-1179-9EA1-B37CDB1B0CCF}.Debug|x64.ActiveCfg = Debug|Any CPU + {8B7674B2-8E1E-1179-9EA1-B37CDB1B0CCF}.Debug|x64.Build.0 = Debug|Any CPU + {8B7674B2-8E1E-1179-9EA1-B37CDB1B0CCF}.Debug|x86.ActiveCfg = Debug|Any CPU + {8B7674B2-8E1E-1179-9EA1-B37CDB1B0CCF}.Debug|x86.Build.0 = Debug|Any CPU {8B7674B2-8E1E-1179-9EA1-B37CDB1B0CCF}.Release|Any CPU.ActiveCfg = Release|Any CPU {8B7674B2-8E1E-1179-9EA1-B37CDB1B0CCF}.Release|Any CPU.Build.0 = Release|Any CPU + {8B7674B2-8E1E-1179-9EA1-B37CDB1B0CCF}.Release|x64.ActiveCfg = Release|Any CPU + {8B7674B2-8E1E-1179-9EA1-B37CDB1B0CCF}.Release|x64.Build.0 = Release|Any CPU + {8B7674B2-8E1E-1179-9EA1-B37CDB1B0CCF}.Release|x86.ActiveCfg = Release|Any CPU + {8B7674B2-8E1E-1179-9EA1-B37CDB1B0CCF}.Release|x86.Build.0 = Release|Any CPU {03CC54E0-D276-5C7F-3A4F-C1E4774712C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {03CC54E0-D276-5C7F-3A4F-C1E4774712C8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {03CC54E0-D276-5C7F-3A4F-C1E4774712C8}.Debug|x64.ActiveCfg = Debug|Any CPU + {03CC54E0-D276-5C7F-3A4F-C1E4774712C8}.Debug|x64.Build.0 = Debug|Any CPU + {03CC54E0-D276-5C7F-3A4F-C1E4774712C8}.Debug|x86.ActiveCfg = Debug|Any CPU + {03CC54E0-D276-5C7F-3A4F-C1E4774712C8}.Debug|x86.Build.0 = Debug|Any CPU {03CC54E0-D276-5C7F-3A4F-C1E4774712C8}.Release|Any CPU.ActiveCfg = Release|Any CPU {03CC54E0-D276-5C7F-3A4F-C1E4774712C8}.Release|Any CPU.Build.0 = Release|Any CPU + {03CC54E0-D276-5C7F-3A4F-C1E4774712C8}.Release|x64.ActiveCfg = Release|Any CPU + {03CC54E0-D276-5C7F-3A4F-C1E4774712C8}.Release|x64.Build.0 = Release|Any CPU + {03CC54E0-D276-5C7F-3A4F-C1E4774712C8}.Release|x86.ActiveCfg = Release|Any CPU + {03CC54E0-D276-5C7F-3A4F-C1E4774712C8}.Release|x86.Build.0 = Release|Any CPU {C8819CD8-47C8-707B-1EAF-6B7E55451C08}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C8819CD8-47C8-707B-1EAF-6B7E55451C08}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C8819CD8-47C8-707B-1EAF-6B7E55451C08}.Debug|x64.ActiveCfg = Debug|Any CPU + {C8819CD8-47C8-707B-1EAF-6B7E55451C08}.Debug|x64.Build.0 = Debug|Any CPU + {C8819CD8-47C8-707B-1EAF-6B7E55451C08}.Debug|x86.ActiveCfg = Debug|Any CPU + {C8819CD8-47C8-707B-1EAF-6B7E55451C08}.Debug|x86.Build.0 = Debug|Any CPU {C8819CD8-47C8-707B-1EAF-6B7E55451C08}.Release|Any CPU.ActiveCfg = Release|Any CPU {C8819CD8-47C8-707B-1EAF-6B7E55451C08}.Release|Any CPU.Build.0 = Release|Any CPU + {C8819CD8-47C8-707B-1EAF-6B7E55451C08}.Release|x64.ActiveCfg = Release|Any CPU + {C8819CD8-47C8-707B-1EAF-6B7E55451C08}.Release|x64.Build.0 = Release|Any CPU + {C8819CD8-47C8-707B-1EAF-6B7E55451C08}.Release|x86.ActiveCfg = Release|Any CPU + {C8819CD8-47C8-707B-1EAF-6B7E55451C08}.Release|x86.Build.0 = Release|Any CPU {EA568308-EFF1-BB6F-8BA8-0DFE8AB9F45D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EA568308-EFF1-BB6F-8BA8-0DFE8AB9F45D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EA568308-EFF1-BB6F-8BA8-0DFE8AB9F45D}.Debug|x64.ActiveCfg = Debug|Any CPU + {EA568308-EFF1-BB6F-8BA8-0DFE8AB9F45D}.Debug|x64.Build.0 = Debug|Any CPU + {EA568308-EFF1-BB6F-8BA8-0DFE8AB9F45D}.Debug|x86.ActiveCfg = Debug|Any CPU + {EA568308-EFF1-BB6F-8BA8-0DFE8AB9F45D}.Debug|x86.Build.0 = Debug|Any CPU {EA568308-EFF1-BB6F-8BA8-0DFE8AB9F45D}.Release|Any CPU.ActiveCfg = Release|Any CPU {EA568308-EFF1-BB6F-8BA8-0DFE8AB9F45D}.Release|Any CPU.Build.0 = Release|Any CPU + {EA568308-EFF1-BB6F-8BA8-0DFE8AB9F45D}.Release|x64.ActiveCfg = Release|Any CPU + {EA568308-EFF1-BB6F-8BA8-0DFE8AB9F45D}.Release|x64.Build.0 = Release|Any CPU + {EA568308-EFF1-BB6F-8BA8-0DFE8AB9F45D}.Release|x86.ActiveCfg = Release|Any CPU + {EA568308-EFF1-BB6F-8BA8-0DFE8AB9F45D}.Release|x86.Build.0 = Release|Any CPU + {CD38901C-17D2-4DA6-A61D-96AFE0B432E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CD38901C-17D2-4DA6-A61D-96AFE0B432E9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CD38901C-17D2-4DA6-A61D-96AFE0B432E9}.Debug|x64.ActiveCfg = Debug|Any CPU + {CD38901C-17D2-4DA6-A61D-96AFE0B432E9}.Debug|x64.Build.0 = Debug|Any CPU + {CD38901C-17D2-4DA6-A61D-96AFE0B432E9}.Debug|x86.ActiveCfg = Debug|Any CPU + {CD38901C-17D2-4DA6-A61D-96AFE0B432E9}.Debug|x86.Build.0 = Debug|Any CPU + {CD38901C-17D2-4DA6-A61D-96AFE0B432E9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CD38901C-17D2-4DA6-A61D-96AFE0B432E9}.Release|Any CPU.Build.0 = Release|Any CPU + {CD38901C-17D2-4DA6-A61D-96AFE0B432E9}.Release|x64.ActiveCfg = Release|Any CPU + {CD38901C-17D2-4DA6-A61D-96AFE0B432E9}.Release|x64.Build.0 = Release|Any CPU + {CD38901C-17D2-4DA6-A61D-96AFE0B432E9}.Release|x86.ActiveCfg = Release|Any CPU + {CD38901C-17D2-4DA6-A61D-96AFE0B432E9}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -202,6 +380,7 @@ Global {03CC54E0-D276-5C7F-3A4F-C1E4774712C8} = {7345D7EF-E4F8-45A0-85AC-E0099B1C1E00} {C8819CD8-47C8-707B-1EAF-6B7E55451C08} = {7345D7EF-E4F8-45A0-85AC-E0099B1C1E00} {EA568308-EFF1-BB6F-8BA8-0DFE8AB9F45D} = {7345D7EF-E4F8-45A0-85AC-E0099B1C1E00} + {CD38901C-17D2-4DA6-A61D-96AFE0B432E9} = {BDF1BC57-DCD1-4EF4-AF95-D58DC60C3FDD} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {810358C4-4439-45AB-AD48-BAFE6A6D611D} diff --git a/Modules/Sales/Sales.Services.UnitTests/CustomerServiceTests.cs b/Modules/Sales/Sales.Services.UnitTests/CustomerServiceTests.cs new file mode 100644 index 0000000..ad17037 --- /dev/null +++ b/Modules/Sales/Sales.Services.UnitTests/CustomerServiceTests.cs @@ -0,0 +1,213 @@ +using DataAccess; +using FluentAssertions; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Sales.DataModel.SalesLT; +using Sales.DataModel.Values; +using Contracts.Sales; + +namespace Sales.Services.UnitTests; + +public class CustomerServiceTests +{ + [Fact] + public void GetCustomersWithOverdueOrders_WithNoOverdueOrders_ReturnsEmptyArray() + { + var repositoryStub = new FakeRepository(Array.Empty()); + var target = GetTarget(repositoryStub); + + var result = target.GetCustomersWithOverdueOrders(); + + result.Should().BeEmpty(); + } + + [Fact] + public void GetCustomersWithOverdueOrders_WithOverdueOrders_ReturnsCustomers() + { + var customer = CreateCustomer(1, "John", "Doe", "Acme Corp"); + var overdueOrder = CreateOrder(1, customer, DateTime.Today.AddDays(-5), SalesOrderHeaderStatusValues.InProcess); + var repositoryStub = new FakeRepository(new[] { overdueOrder }); + var target = GetTarget(repositoryStub); + + var result = target.GetCustomersWithOverdueOrders(); + + result.Should().ContainSingle(c => c.CustomerName == "Acme Corp" && c.OverdueOrderCount == 1); + } + + [Fact] + public void GetCustomersWithOverdueOrders_WithMultipleOverduePerCustomer_AggregatesCorrectly() + { + var customer = CreateCustomer(1, "Jane", "Smith", "Beta Inc"); + var order1 = CreateOrder(1, customer, DateTime.Today.AddDays(-10), SalesOrderHeaderStatusValues.InProcess); + var order2 = CreateOrder(2, customer, DateTime.Today.AddDays(-5), SalesOrderHeaderStatusValues.Approved); + var order3 = CreateOrder(3, customer, DateTime.Today.AddDays(-3), SalesOrderHeaderStatusValues.Backordered); + var repositoryStub = new FakeRepository(new[] { order1, order2, order3 }); + var target = GetTarget(repositoryStub); + + var result = target.GetCustomersWithOverdueOrders(); + + result.Should().ContainSingle(c => + c.CustomerName == "Beta Inc" && + c.OverdueOrderCount == 3 && + c.OldestOverdueOrderDate == DateTime.Today.AddDays(-10)); + } + + [Fact] + public void GetCustomersWithOverdueOrders_FiltersByStatusShipped_ExcludesShippedOrders() + { + var customer = CreateCustomer(1, "Bob", "Jones", "Gamma Ltd"); + var overdueOrder = CreateOrder(1, customer, DateTime.Today.AddDays(-5), SalesOrderHeaderStatusValues.InProcess); + var shippedOrder = CreateOrder(2, customer, DateTime.Today.AddDays(-10), SalesOrderHeaderStatusValues.Shipped); + var repositoryStub = new FakeRepository(new[] { overdueOrder, shippedOrder }); + var target = GetTarget(repositoryStub); + + var result = target.GetCustomersWithOverdueOrders(); + + result.Should().ContainSingle(c => + c.CustomerName == "Gamma Ltd" && + c.OverdueOrderCount == 1 && + c.OldestOverdueOrderDate == DateTime.Today.AddDays(-5)); + } + + [Fact] + public void GetCustomersWithOverdueOrders_FiltersByStatusCancelled_ExcludesCancelledOrders() + { + var customer = CreateCustomer(1, "Alice", "Brown", "Delta Co"); + var overdueOrder = CreateOrder(1, customer, DateTime.Today.AddDays(-7), SalesOrderHeaderStatusValues.InProcess); + var cancelledOrder = CreateOrder(2, customer, DateTime.Today.AddDays(-15), SalesOrderHeaderStatusValues.Cancelled); + var repositoryStub = new FakeRepository(new[] { overdueOrder, cancelledOrder }); + var target = GetTarget(repositoryStub); + + var result = target.GetCustomersWithOverdueOrders(); + + result.Should().ContainSingle(c => + c.CustomerName == "Delta Co" && + c.OverdueOrderCount == 1 && + c.OldestOverdueOrderDate == DateTime.Today.AddDays(-7)); + } + + [Fact] + public void GetCustomersWithOverdueOrders_FiltersByDueDate_ExcludesFutureOrders() + { + var customer = CreateCustomer(1, "Tom", "Green", "Epsilon Corp"); + var overdueOrder = CreateOrder(1, customer, DateTime.Today.AddDays(-3), SalesOrderHeaderStatusValues.InProcess); + var futureOrder = CreateOrder(2, customer, DateTime.Today.AddDays(5), SalesOrderHeaderStatusValues.InProcess); + var repositoryStub = new FakeRepository(new[] { overdueOrder, futureOrder }); + var target = GetTarget(repositoryStub); + + var result = target.GetCustomersWithOverdueOrders(); + + result.Should().ContainSingle(c => + c.CustomerName == "Epsilon Corp" && + c.OverdueOrderCount == 1 && + c.OldestOverdueOrderDate == DateTime.Today.AddDays(-3)); + } + + [Fact] + public void GetCustomersWithOverdueOrders_SortsByOldestDueDate_Ascending() + { + var customer1 = CreateCustomer(1, "Alex", "White", "Zeta Inc"); + var customer2 = CreateCustomer(2, "Chris", "Black", "Alpha Corp"); + var customer3 = CreateCustomer(3, "Dana", "Gray", "Mega Ltd"); + + var order1 = CreateOrder(1, customer1, DateTime.Today.AddDays(-3), SalesOrderHeaderStatusValues.InProcess); + var order2 = CreateOrder(2, customer2, DateTime.Today.AddDays(-10), SalesOrderHeaderStatusValues.InProcess); + var order3 = CreateOrder(3, customer3, DateTime.Today.AddDays(-5), SalesOrderHeaderStatusValues.InProcess); + + var repositoryStub = new FakeRepository(new[] { order1, order2, order3 }); + var target = GetTarget(repositoryStub); + + var result = target.GetCustomersWithOverdueOrders(); + + var expected = new[] + { + new CustomerOverdueOrdersData + { + CustomerName = "Alpha Corp", + OverdueOrderCount = 1, + OldestOverdueOrderDate = DateTime.Today.AddDays(-10) + }, + new CustomerOverdueOrdersData + { + CustomerName = "Mega Ltd", + OverdueOrderCount = 1, + OldestOverdueOrderDate = DateTime.Today.AddDays(-5) + }, + new CustomerOverdueOrdersData + { + CustomerName = "Zeta Inc", + OverdueOrderCount = 1, + OldestOverdueOrderDate = DateTime.Today.AddDays(-3) + } + }; + + result.Should().BeEquivalentTo(expected, options => options + .Including(c => c.CustomerName) + .Including(c => c.OverdueOrderCount) + .Including(c => c.OldestOverdueOrderDate) + .WithStrictOrdering()); + } + + [Fact] + public void GetCustomersWithOverdueOrders_HandlesNullCompanyName_UsesFirstLastName() + { + var customer = CreateCustomer(1, "Sarah", "Wilson", null); + var overdueOrder = CreateOrder(1, customer, DateTime.Today.AddDays(-2), SalesOrderHeaderStatusValues.InProcess); + var repositoryStub = new FakeRepository(new[] { overdueOrder }); + var target = GetTarget(repositoryStub); + + var result = target.GetCustomersWithOverdueOrders(); + + result.Should().ContainSingle(c => c.CustomerName == "Sarah Wilson"); + } + + // ── Helpers ──────────────────────────────────────────────── + + private static CustomerService GetTarget(FakeRepository repositoryStub) + => new CustomerService(repositoryStub, Substitute.For>()); + + private static Customer CreateCustomer(int id, string firstName, string lastName, string? companyName) + => new Customer + { + CustomerID = id, + FirstName = firstName, + LastName = lastName, + CompanyName = companyName, + SalesOrderHeaders = new List() + }; + + private static SalesOrderHeader CreateOrder(int id, Customer customer, DateTime dueDate, byte status) + { + var order = new SalesOrderHeader + { + SalesOrderID = id, + CustomerID = customer.CustomerID, + Customer = customer, + DueDate = dueDate, + Status = status, + OrderDate = DateTime.Today.AddDays(-30), + ShipMethod = "CARGO TRANSPORT 5", + RevisionNumber = 1, + SalesOrderNumber = $"SO{id}" + }; + customer.SalesOrderHeaders.Add(order); + return order; + } + + private class FakeRepository : IRepository + { + private readonly List data = new(); + + public FakeRepository(SalesOrderHeader[] orders) + { + data.AddRange(orders); + } + + public IQueryable GetEntities() where T : class + { + return data.OfType().AsQueryable(); + } + + public IUnitOfWork CreateUnitOfWork() => throw new NotImplementedException(); + } +} 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..88e8adc --- /dev/null +++ b/Modules/Sales/Sales.Services.UnitTests/Sales.Services.UnitTests.csproj @@ -0,0 +1,38 @@ + + + + net10.0 + enable + enable + Exe + false + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + diff --git a/Modules/Sales/Sales.Services.UnitTests/xunit.runner.json b/Modules/Sales/Sales.Services.UnitTests/xunit.runner.json new file mode 100644 index 0000000..cb69c43 --- /dev/null +++ b/Modules/Sales/Sales.Services.UnitTests/xunit.runner.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", + "methodDisplay": "method" +} diff --git a/Modules/Sales/Sales.Services/Sales.Services.csproj b/Modules/Sales/Sales.Services/Sales.Services.csproj index 68dae67..3e1ff46 100644 --- a/Modules/Sales/Sales.Services/Sales.Services.csproj +++ b/Modules/Sales/Sales.Services/Sales.Services.csproj @@ -14,4 +14,9 @@ + + + + +