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
17 changes: 5 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,13 @@ Build, including financial chart details from Aggie Enterprise.

| Method | Route | Authentication | Response |
| --- | --- | --- | --- |
| `GET` | `/api/health` | None | `{"status":"healthy","service":"KOI","version":"0.1.2","revision":"<git-sha-or-local>"}` |
| `GET` | `/api/health` | None | `{"status":"healthy","service":"KOI","version":"0.1.3","revision":"<git-sha-or-local>"}` |
| `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 |
| `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 |

The `details` endpoints return a flattened subset of the Aggie Enterprise data,
while `full-details` preserves the complete response. Financial responses include
`chartType`; flattened details include `validationStatus`, while full details and
validation results include the same display-ready text as `message` for Kuali
Build. Chart types are returned as `GL`, `PPM`, or `INVALID`.

The financial details endpoint returns a flattened subset of the Aggie Enterprise
data. Its response includes `chartType` and the display-ready `validationStatus`
for 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
2 changes: 1 addition & 1 deletion docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ The script never sends a plaintext KOI bearer token to GitHub or Azure.

## Aggie Enterprise Financial configuration

The Financial endpoints require six Aggie Enterprise settings. Add the values
The Financial endpoint requires six Aggie Enterprise settings. Add the values
for one environment to its local handoff file:

```dotenv
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.2","revision":"local"}' \
'{"status":"healthy","service":"KOI","version":"0.1.3","revision":"local"}' \
"$health_status" \
"$health_file"

Expand Down
220 changes: 0 additions & 220 deletions src/Koi.Functions/Financial/FinancialFunction.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
using System.Net;
using System.Text.Json;
using Koi.Functions.Financial.Models;
using Koi.Functions.Financial.Services;
using Koi.Functions.Http;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;

namespace Koi.Functions.Financial;

public sealed class FinancialFunction
{
internal const int MaxBatchSize = 50;
internal const int MaxConcurrency = 5;

private readonly IAggieEnterpriseService _aggieEnterpriseService;

public FinancialFunction(IAggieEnterpriseService aggieEnterpriseService)
Expand All @@ -21,11 +16,6 @@ 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";

[Function(FunctionName)]
public async Task<HttpResponseData> Run(
Expand All @@ -40,214 +30,4 @@ public async Task<HttpResponseData> Run(
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();
response.StatusCode = HttpStatusCode.OK;
await response.WriteAsJsonAsync(aeDetails, cancellationToken);
return response;
}

[Function(BulkFunctionName)]
public async Task<HttpResponseData> RunBulk(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "v1/financial/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 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();
response.StatusCode = HttpStatusCode.OK;
await response.WriteAsJsonAsync(aeDetails, cancellationToken);
return response;
}

[Function(ValidationFunctionName)]
public async Task<HttpResponseData> RunValidation(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "v1/financial/validate/{value}")] HttpRequestData request,
string value,
CancellationToken cancellationToken)
{
var result = await _aggieEnterpriseService.ValidateAsync(value, cancellationToken);
var response = request.CreateResponse();
response.StatusCode = HttpStatusCode.OK;
await response.WriteAsJsonAsync(result, cancellationToken);
return response;
}

[Function(BulkValidationFunctionName)]
public async Task<HttpResponseData> RunBulkValidation(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "v1/financial/validate")] 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 validationResults = await ValidateAsync(chartStrings, cancellationToken);

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

private static async Task<HttpResponseData> CreateInvalidBodyResponseAsync(
HttpRequestData request,
CancellationToken cancellationToken)
{
return await CreateBadRequestResponseAsync(
request,
"request body must be a JSON array of chart strings",
cancellationToken);
}

private async Task<AeDetails[]> GetAeDetailsAsync(
string[] chartStrings,
CancellationToken cancellationToken)
{
var results = new AeDetails[chartStrings.Length];

await Parallel.ForEachAsync(
Enumerable.Range(0, chartStrings.Length),
new ParallelOptions
{
CancellationToken = cancellationToken,
MaxDegreeOfParallelism = MaxConcurrency
},
async (index, iterationCancellationToken) =>
{
results[index] = await _aggieEnterpriseService.GetAeDetailsAsync(
chartStrings[index],
iterationCancellationToken);
});

return results;
}

private async Task<FinancialValidationResult[]> ValidateAsync(
string[] chartStrings,
CancellationToken cancellationToken)
{
var results = new FinancialValidationResult[chartStrings.Length];

await Parallel.ForEachAsync(
Enumerable.Range(0, chartStrings.Length),
new ParallelOptions
{
CancellationToken = cancellationToken,
MaxDegreeOfParallelism = MaxConcurrency
},
async (index, iterationCancellationToken) =>
{
results[index] = await _aggieEnterpriseService.ValidateAsync(
chartStrings[index],
iterationCancellationToken);
});

return results;
}

private static async Task<HttpResponseData> CreateBadRequestResponseAsync(
HttpRequestData request,
string error,
CancellationToken cancellationToken)
{
var response = request.CreateResponse();
response.StatusCode = HttpStatusCode.BadRequest;
await response.WriteAsJsonAsync(
new ErrorResponse(error),
cancellationToken);
return response;
}
}
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.2</Version>
<Version>0.1.3</Version>
</PropertyGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,36 +27,6 @@ public void FinancialIsAuthenticatedByDefault()
Assert.False(_policy.IsAnonymous(FinancialFunction.FunctionName));
}

[Fact]
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()
{
Assert.False(_policy.IsAnonymous(FinancialFunction.ValidationFunctionName));
}

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

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