Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,20 @@ Build, including financial chart details from Aggie Enterprise.

| Method | Route | Authentication | Response |
| --- | --- | --- | --- |
| `GET` | `/api/health` | None | `{"status":"healthy","service":"KOI","version":"0.1.1","revision":"<git-sha-or-local>"}` |
| `GET` | `/api/health` | None | `{"status":"healthy","service":"KOI","version":"0.1.2","revision":"<git-sha-or-local>"}` |
| `GET` | `/api/v1/hello` | Bearer token | `{"message":"Hello from KOI"}` |
| `GET` | `/api/v1/financial/details/{value}` | Bearer token | Financial details for one chart string |
| `POST` | `/api/v1/financial/details` | Bearer token | Financial details for an array of chart strings |
| `GET` | `/api/v1/financial/details/{value}` | Bearer token | Flattened financial details for one chart string |
| `POST` | `/api/v1/financial/details` | Bearer token | Flattened financial details for an array of chart strings |
| `GET` | `/api/v1/financial/full-details/{value}` | Bearer token | Full financial details for one chart string |
| `POST` | `/api/v1/financial/full-details` | Bearer token | Full financial details for an array of chart strings |
| `GET` | `/api/v1/financial/validate/{value}` | Bearer token | Validation result for one chart string |
| `POST` | `/api/v1/financial/validate` | Bearer token | Validation results for an array of chart strings |

Financial details and validation results include `chartType` and `message` for
direct display in Kuali Build. Chart types are returned as `GL`, `PPM`, or
`INVALID`. Valid GL and PPM results return
The `details` endpoints return a flattened subset of the Aggie Enterprise data,
while `full-details` preserves the complete response. Financial responses include
`chartType`; full details and validation results also include `message` for direct
display in Kuali Build. Chart types are returned as `GL`, `PPM`, or `INVALID`.
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.`

Expand Down
4 changes: 2 additions & 2 deletions scripts/smoke-authenticated.sh
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,10 @@ fi

if ! jq --exit-status \
--arg chart_string "$financial_chart_string" \
'.chartString == $chart_string and .isValid == true and (.errors | length == 0)' \
'.chartString == $chart_string and .isValid == true and .error == ""' \
"$financial_body" >/dev/null; then
echo "Financial integration returned 200 without a valid Aggie Enterprise response." >&2
jq '{chartString, chartStringType, isValid, error, warning}' "$financial_body" >&2
jq '{chartString, chartType, isValid, error, warning}' "$financial_body" >&2
exit 1
fi

Expand Down
2 changes: 1 addition & 1 deletion scripts/smoke-local.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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.1","revision":"local"}' \
'{"status":"healthy","service":"KOI","version":"0.1.2","revision":"local"}' \
"$health_status" \
"$health_file"

Expand Down
55 changes: 55 additions & 0 deletions src/Koi.Functions/Financial/FinancialFunction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ public FinancialFunction(IAggieEnterpriseService aggieEnterpriseService)

public const string FunctionName = "Financial";
public const string BulkFunctionName = "FinancialBulk";
public const string FullDetailsFunctionName = "FinancialFullDetails";
public const string BulkFullDetailsFunctionName = "FinancialFullDetailsBulk";
public const string ValidationFunctionName = "FinancialValidation";
public const string BulkValidationFunctionName = "FinancialValidationBulk";

Expand All @@ -30,6 +32,20 @@ public async Task<HttpResponseData> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "v1/financial/details/{value}")] HttpRequestData request,
string value,
CancellationToken cancellationToken)
{
var aeDetails = await _aggieEnterpriseService.GetAeDetailsAsync(value, cancellationToken);
var financialDetails = FinancialDetails.FromAeDetails(aeDetails);
var response = request.CreateResponse();
response.StatusCode = HttpStatusCode.OK;
await response.WriteAsJsonAsync(financialDetails, cancellationToken);
return response;
}

[Function(FullDetailsFunctionName)]
public async Task<HttpResponseData> RunFullDetails(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "v1/financial/full-details/{value}")] HttpRequestData request,
string value,
CancellationToken cancellationToken)
{
var aeDetails = await _aggieEnterpriseService.GetAeDetailsAsync(value, cancellationToken);
var response = request.CreateResponse();
Expand Down Expand Up @@ -68,6 +84,45 @@ public async Task<HttpResponseData> RunBulk(
cancellationToken);
}

var aeDetails = await GetAeDetailsAsync(chartStrings, cancellationToken);
var financialDetails = aeDetails.Select(FinancialDetails.FromAeDetails).ToArray();

var response = request.CreateResponse();
response.StatusCode = HttpStatusCode.OK;
await response.WriteAsJsonAsync(financialDetails, cancellationToken);
return response;
}

[Function(BulkFullDetailsFunctionName)]
public async Task<HttpResponseData> RunBulkFullDetails(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "v1/financial/full-details")] HttpRequestData request,
CancellationToken cancellationToken)
{
string[]? chartStrings;
try
{
chartStrings = await JsonSerializer.DeserializeAsync<string[]>(
request.Body,
cancellationToken: cancellationToken);
}
catch (JsonException)
{
return await CreateInvalidBodyResponseAsync(request, cancellationToken);
}

if (chartStrings is null)
{
return await CreateInvalidBodyResponseAsync(request, cancellationToken);
}

if (chartStrings.Length > MaxBatchSize)
{
return await CreateBadRequestResponseAsync(
request,
$"request body must contain no more than {MaxBatchSize} chart strings",
cancellationToken);
}

var aeDetails = await GetAeDetailsAsync(chartStrings, cancellationToken);

var response = request.CreateResponse();
Expand Down
104 changes: 104 additions & 0 deletions src/Koi.Functions/Financial/Models/FinancialDetails.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
namespace Koi.Functions.Financial.Models;

public sealed class FinancialDetails
{
private const string GlFinancialDepartmentEntity = "GL Financial Department";
private const string AwardEntity = "Award";
private const string PrincipalInvestigatorRole = "Principal Investigator";
private const string ProjectManagerRole = "Project Manager";
private const string ProjectRoleType = "P";

public bool IsValid { get; set; }

public string ChartType { get; set; } = string.Empty;

public string ChartString { get; set; } = string.Empty;

public string Error { get; set; } = string.Empty;

public string Warning { get; set; } = string.Empty;

public string GlFinancialDepartmentName { get; set; } = string.Empty;

public string ProjectStartDate { get; set; } = string.Empty;

public string ProjectCompletionDate { get; set; } = string.Empty;

public string AwardStatus { get; set; } = string.Empty;

public string AwardStartDate { get; set; } = string.Empty;

public string AwardEndDate { get; set; } = string.Empty;

public string AwardInfo { get; set; } = string.Empty;

public string ProjectTypeName { get; set; } = string.Empty;

public string PrincipalInvestigatorName { get; set; } = string.Empty;

public string PrincipalInvestigatorEmail { get; set; } = string.Empty;

public string ProjectManagerName { get; set; } = string.Empty;

public string ProjectManagerEmail { get; set; } = string.Empty;

public string FundPurpose { get; set; } = string.Empty;

public static FinancialDetails FromAeDetails(AeDetails aeDetails)
{
ArgumentNullException.ThrowIfNull(aeDetails);

var principalInvestigator = FindProjectRoleApprover(
aeDetails,
PrincipalInvestigatorRole);
var projectManager = FindProjectRoleApprover(aeDetails, ProjectManagerRole);

return new FinancialDetails
{
IsValid = aeDetails.IsValid,
ChartType = aeDetails.ChartType,
ChartString = aeDetails.ChartString,
Error = aeDetails.Error,
Warning = aeDetails.Warning,
GlFinancialDepartmentName = FindSegmentName(
aeDetails,
GlFinancialDepartmentEntity),
ProjectStartDate = aeDetails.PpmDetails?.ProjectStartDate ?? string.Empty,
ProjectCompletionDate = aeDetails.PpmDetails?.ProjectCompletionDate ?? string.Empty,
AwardStatus = aeDetails.PpmDetails?.AwardStatus ?? string.Empty,
AwardStartDate = aeDetails.PpmDetails?.AwardStartDate ?? string.Empty,
AwardEndDate = aeDetails.PpmDetails?.AwardEndDate ?? string.Empty,
AwardInfo = FindSegmentName(aeDetails, AwardEntity),
ProjectTypeName = aeDetails.PpmDetails?.ProjectTypeName ?? string.Empty,
PrincipalInvestigatorName = principalInvestigator?.Name ?? string.Empty,
PrincipalInvestigatorEmail = principalInvestigator?.Email ?? string.Empty,
ProjectManagerName = projectManager?.Name ?? string.Empty,
ProjectManagerEmail = projectManager?.Email ?? string.Empty,
FundPurpose = aeDetails.FundPurpose ?? string.Empty
};
}

private static string FindSegmentName(AeDetails aeDetails, string entity)
{
return aeDetails.SegmentDetails
.FirstOrDefault(segment => string.Equals(
segment.Entity,
entity,
StringComparison.Ordinal))
?.Name ?? string.Empty;
}

private static Approver? FindProjectRoleApprover(AeDetails aeDetails, string roleName)
{
var role = aeDetails.PpmDetails?.Roles
.Where(role => string.Equals(role.Type, ProjectRoleType, StringComparison.Ordinal))
.Where(role => string.Equals(
role.RoleName,
roleName,
StringComparison.OrdinalIgnoreCase))
.OrderBy(role => role.Order)
.FirstOrDefault();

return role?.Approvers.FirstOrDefault();
}
}
2 changes: 1 addition & 1 deletion src/Koi.Functions/Koi.Functions.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>Koi.Functions</RootNamespace>
<Version>0.1.1</Version>
<Version>0.1.2</Version>
</PropertyGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@ public void FinancialBulkIsAuthenticatedByDefault()
Assert.False(_policy.IsAnonymous(FinancialFunction.BulkFunctionName));
}

[Fact]
public void FinancialFullDetailsIsAuthenticatedByDefault()
{
Assert.False(_policy.IsAnonymous(FinancialFunction.FullDetailsFunctionName));
}

[Fact]
public void FinancialBulkFullDetailsIsAuthenticatedByDefault()
{
Assert.False(_policy.IsAnonymous(FinancialFunction.BulkFullDetailsFunctionName));
}

[Fact]
public void FinancialValidationIsAuthenticatedByDefault()
{
Expand Down
Loading