From 3761b238a67e06446571ccfb16dcd6909443dc63 Mon Sep 17 00:00:00 2001 From: Jason Sylvestre Date: Thu, 3 Sep 2026 09:05:24 -0700 Subject: [PATCH] feat(financial): expose Aggie Enterprise segment display names Provides top-level fields for GL (entity, fund, department, account, purpose, program, project, activity) and PPM (project, task, expenditure organization, expenditure type) segment names in financial details. This simplifies client consumption of descriptive segment information without needing to parse the raw segment details array. Updates API version to 0.1.4. --- README.md | 8 +- scripts/smoke-local.sh | 2 +- .../Financial/Models/FinancialDetails.cs | 46 +++++++++++ src/Koi.Functions/Koi.Functions.csproj | 2 +- .../Financial/FinancialFunctionTests.cs | 46 +++++++++++ .../ResponseContractTests.cs | 81 ++++++++++++++++++- 6 files changed, 181 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a2a039a..e40078e 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Build, including financial chart details from Aggie Enterprise. | Method | Route | Authentication | Response | | --- | --- | --- | --- | -| `GET` | `/api/health` | None | `{"status":"healthy","service":"KOI","version":"0.1.3","revision":""}` | +| `GET` | `/api/health` | None | `{"status":"healthy","service":"KOI","version":"0.1.4","revision":""}` | | `GET` | `/api/v1/hello` | Bearer token | `{"message":"Hello from KOI"}` | | `GET` | `/api/v1/financial/details/{value}` | Bearer token | Flattened financial details for one chart string | @@ -18,6 +18,12 @@ Valid GL and PPM results return `This is a valid GL chart string.` and `This is a valid PPM chart string.`, respectively. Invalid results return `This is not a valid chart string.` +Segment display names are sourced from the Aggie Enterprise segment details. +GL responses populate `entityName`, `fundName`, `departmentName`, `accountName`, +`purposeName`, `programName`, `projectName`, and `activityName`. PPM responses +populate `projectName`, `taskName`, `expenditureOrganizationName`, and +`expenditureTypeName`. Segment-name fields that do not apply are empty strings. + All HTTP functions require authentication by default. The health function is the only explicit anonymous exception. diff --git a/scripts/smoke-local.sh b/scripts/smoke-local.sh index c586f70..af320d1 100755 --- a/scripts/smoke-local.sh +++ b/scripts/smoke-local.sh @@ -46,7 +46,7 @@ health_status="$(request "$health_file" "$base_url/api/health")" assert_response \ "Anonymous health" \ 200 \ - '{"status":"healthy","service":"KOI","version":"0.1.3","revision":"local"}' \ + '{"status":"healthy","service":"KOI","version":"0.1.4","revision":"local"}' \ "$health_status" \ "$health_file" diff --git a/src/Koi.Functions/Financial/Models/FinancialDetails.cs b/src/Koi.Functions/Financial/Models/FinancialDetails.cs index 7886f94..d6e6c06 100644 --- a/src/Koi.Functions/Financial/Models/FinancialDetails.cs +++ b/src/Koi.Functions/Financial/Models/FinancialDetails.cs @@ -2,6 +2,17 @@ namespace Koi.Functions.Financial.Models; public sealed class FinancialDetails { + private const string EntitySegment = "Entity"; + private const string FundSegment = "Fund"; + private const string DepartmentSegment = "Department"; + private const string AccountSegment = "Account"; + private const string PurposeSegment = "Purpose"; + private const string ProgramSegment = "Program"; + private const string ProjectSegment = "Project"; + private const string ActivitySegment = "Activity"; + private const string TaskSegment = "Task"; + private const string ExpenditureOrganizationSegment = "Expenditure Organization"; + private const string ExpenditureTypeSegment = "Expenditure Type"; private const string GlFinancialDepartmentEntity = "GL Financial Department"; private const string AwardEntity = "Award"; private const string PrincipalInvestigatorRole = "Principal Investigator"; @@ -20,6 +31,28 @@ public sealed class FinancialDetails public string Warning { get; set; } = string.Empty; + public string EntityName { get; set; } = string.Empty; + + public string FundName { get; set; } = string.Empty; + + public string DepartmentName { get; set; } = string.Empty; + + public string AccountName { get; set; } = string.Empty; + + public string PurposeName { get; set; } = string.Empty; + + public string ProgramName { get; set; } = string.Empty; + + public string ProjectName { get; set; } = string.Empty; + + public string ActivityName { get; set; } = string.Empty; + + public string TaskName { get; set; } = string.Empty; + + public string ExpenditureOrganizationName { get; set; } = string.Empty; + + public string ExpenditureTypeName { get; set; } = string.Empty; + public string GlFinancialDepartmentName { get; set; } = string.Empty; public string ProjectStartDate { get; set; } = string.Empty; @@ -63,6 +96,19 @@ public static FinancialDetails FromAeDetails(AeDetails aeDetails) ChartString = aeDetails.ChartString, Error = aeDetails.Error, Warning = aeDetails.Warning, + EntityName = FindSegmentName(aeDetails, EntitySegment), + FundName = FindSegmentName(aeDetails, FundSegment), + DepartmentName = FindSegmentName(aeDetails, DepartmentSegment), + AccountName = FindSegmentName(aeDetails, AccountSegment), + PurposeName = FindSegmentName(aeDetails, PurposeSegment), + ProgramName = FindSegmentName(aeDetails, ProgramSegment), + ProjectName = FindSegmentName(aeDetails, ProjectSegment), + ActivityName = FindSegmentName(aeDetails, ActivitySegment), + TaskName = FindSegmentName(aeDetails, TaskSegment), + ExpenditureOrganizationName = FindSegmentName( + aeDetails, + ExpenditureOrganizationSegment), + ExpenditureTypeName = FindSegmentName(aeDetails, ExpenditureTypeSegment), GlFinancialDepartmentName = FindSegmentName( aeDetails, GlFinancialDepartmentEntity), diff --git a/src/Koi.Functions/Koi.Functions.csproj b/src/Koi.Functions/Koi.Functions.csproj index 6fd159d..e6eb658 100644 --- a/src/Koi.Functions/Koi.Functions.csproj +++ b/src/Koi.Functions/Koi.Functions.csproj @@ -7,7 +7,7 @@ enable enable Koi.Functions - 0.1.3 + 0.1.4 diff --git a/tests/Koi.Functions.Tests/Financial/FinancialFunctionTests.cs b/tests/Koi.Functions.Tests/Financial/FinancialFunctionTests.cs index c23b722..eb9912d 100644 --- a/tests/Koi.Functions.Tests/Financial/FinancialFunctionTests.cs +++ b/tests/Koi.Functions.Tests/Financial/FinancialFunctionTests.cs @@ -68,6 +68,26 @@ public async Task RunReturnsOnlyFlatMappedFinancialDetails() Entity = "Award", Code = "A232815", Name = "A232815 SP0A232815 PO2610129 Electrochemically Mediated Air Separation Modules (EM-ASM)" + }, + new SegmentDetails + { + Entity = "Project", + Name = "Air Separation Project" + }, + new SegmentDetails + { + Entity = "Task", + Name = "Prototype Development" + }, + new SegmentDetails + { + Entity = "Expenditure Organization", + Name = "Chemical Engineering" + }, + new SegmentDetails + { + Entity = "Expenditure Type", + Name = "Research Supplies" } ], Approvers = @@ -156,6 +176,17 @@ public async Task RunReturnsOnlyFlatMappedFinancialDetails() "error", "warning", "glFinancialDepartmentName", + "entityName", + "fundName", + "departmentName", + "accountName", + "purposeName", + "programName", + "projectName", + "activityName", + "taskName", + "expenditureOrganizationName", + "expenditureTypeName", "projectStartDate", "projectCompletionDate", "awardStatus", @@ -188,6 +219,21 @@ public async Task RunReturnsOnlyFlatMappedFinancialDetails() Assert.Equal( "Biological Sciences", root.GetProperty("glFinancialDepartmentName").GetString()); + Assert.Equal(string.Empty, root.GetProperty("entityName").GetString()); + Assert.Equal(string.Empty, root.GetProperty("fundName").GetString()); + Assert.Equal(string.Empty, root.GetProperty("departmentName").GetString()); + Assert.Equal(string.Empty, root.GetProperty("accountName").GetString()); + Assert.Equal(string.Empty, root.GetProperty("purposeName").GetString()); + Assert.Equal(string.Empty, root.GetProperty("programName").GetString()); + Assert.Equal("Air Separation Project", root.GetProperty("projectName").GetString()); + Assert.Equal(string.Empty, root.GetProperty("activityName").GetString()); + Assert.Equal("Prototype Development", root.GetProperty("taskName").GetString()); + Assert.Equal( + "Chemical Engineering", + root.GetProperty("expenditureOrganizationName").GetString()); + Assert.Equal( + "Research Supplies", + root.GetProperty("expenditureTypeName").GetString()); Assert.Equal("2025-01-01", root.GetProperty("projectStartDate").GetString()); Assert.Equal("2026-12-31", root.GetProperty("projectCompletionDate").GetString()); Assert.Equal("Active", root.GetProperty("awardStatus").GetString()); diff --git a/tests/Koi.Functions.Tests/ResponseContractTests.cs b/tests/Koi.Functions.Tests/ResponseContractTests.cs index 2605da4..26af32e 100644 --- a/tests/Koi.Functions.Tests/ResponseContractTests.cs +++ b/tests/Koi.Functions.Tests/ResponseContractTests.cs @@ -18,7 +18,7 @@ public void HealthContractIsStable() Assert.Equal("healthy", response.Status); Assert.Equal("KOI", response.Service); - Assert.Equal("0.1.3", response.Version); + Assert.Equal("0.1.4", response.Version); Assert.NotEmpty(response.Revision); } @@ -67,6 +67,17 @@ public void FinancialDetailsMapsMissingOptionalValuesToEmptyStrings() Assert.Equal("invalid", response.ChartString); Assert.Equal("Invalid Chart Type", response.Error); Assert.Equal("Example warning", response.Warning); + Assert.Equal(string.Empty, response.EntityName); + Assert.Equal(string.Empty, response.FundName); + Assert.Equal(string.Empty, response.DepartmentName); + Assert.Equal(string.Empty, response.AccountName); + Assert.Equal(string.Empty, response.PurposeName); + Assert.Equal(string.Empty, response.ProgramName); + Assert.Equal(string.Empty, response.ProjectName); + Assert.Equal(string.Empty, response.ActivityName); + Assert.Equal(string.Empty, response.TaskName); + Assert.Equal(string.Empty, response.ExpenditureOrganizationName); + Assert.Equal(string.Empty, response.ExpenditureTypeName); Assert.Equal(string.Empty, response.GlFinancialDepartmentName); Assert.Equal(string.Empty, response.ProjectStartDate); Assert.Equal(string.Empty, response.ProjectCompletionDate); @@ -82,6 +93,74 @@ public void FinancialDetailsMapsMissingOptionalValuesToEmptyStrings() Assert.Equal(string.Empty, response.FundPurpose); } + [Fact] + public void FinancialDetailsMapsGlSegmentNamesFromAeDetails() + { + var response = FinancialDetails.FromAeDetails(new AeDetails + { + ChartStringType = FinancialChartStringType.Gl, + SegmentDetails = + [ + new SegmentDetails { Entity = "Entity", Name = "UC Davis" }, + new SegmentDetails { Entity = "Fund", Name = "General Funds" }, + new SegmentDetails { Entity = "Department", Name = "Computer Science" }, + new SegmentDetails { Entity = "Account", Name = "Supplies" }, + new SegmentDetails { Entity = "Purpose", Name = "Instruction" }, + new SegmentDetails { Entity = "Program", Name = "Academic Programs" }, + new SegmentDetails { Entity = "Project", Name = "Campus Project" }, + new SegmentDetails { Entity = "Activity", Name = "Core Activity" } + ] + }); + + Assert.Equal("UC Davis", response.EntityName); + Assert.Equal("General Funds", response.FundName); + Assert.Equal("Computer Science", response.DepartmentName); + Assert.Equal("Supplies", response.AccountName); + Assert.Equal("Instruction", response.PurposeName); + Assert.Equal("Academic Programs", response.ProgramName); + Assert.Equal("Campus Project", response.ProjectName); + Assert.Equal("Core Activity", response.ActivityName); + Assert.Equal(string.Empty, response.TaskName); + Assert.Equal(string.Empty, response.ExpenditureOrganizationName); + Assert.Equal(string.Empty, response.ExpenditureTypeName); + } + + [Fact] + public void FinancialDetailsMapsPpmSegmentNamesFromAeDetails() + { + var response = FinancialDetails.FromAeDetails(new AeDetails + { + ChartStringType = FinancialChartStringType.Ppm, + SegmentDetails = + [ + new SegmentDetails { Entity = "Project", Name = "Research Project" }, + new SegmentDetails { Entity = "Task", Name = "Project Task" }, + new SegmentDetails + { + Entity = "Expenditure Organization", + Name = "Engineering" + }, + new SegmentDetails + { + Entity = "Expenditure Type", + Name = "Research Supplies" + } + ] + }); + + Assert.Equal("Research Project", response.ProjectName); + Assert.Equal("Project Task", response.TaskName); + Assert.Equal("Engineering", response.ExpenditureOrganizationName); + Assert.Equal("Research Supplies", response.ExpenditureTypeName); + Assert.Equal(string.Empty, response.EntityName); + Assert.Equal(string.Empty, response.FundName); + Assert.Equal(string.Empty, response.DepartmentName); + Assert.Equal(string.Empty, response.AccountName); + Assert.Equal(string.Empty, response.PurposeName); + Assert.Equal(string.Empty, response.ProgramName); + Assert.Equal(string.Empty, response.ActivityName); + } + [Theory] [InlineData( true,