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
35 changes: 32 additions & 3 deletions .github/workflows/main_function-app-traceability-api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,23 @@ on:
push:
branches:
- main
pull_request:
branches:
- main
paths-ignore:
- 'documentation/**'
- '*.md'
workflow_dispatch:

env:
AZURE_FUNCTIONAPP_PACKAGE_PATH: '.' # set this to the path to your web app project, defaults to the repository root
DOTNET_VERSION: '8.0.x' # set this to the dotnet version to use

jobs:
build-and-deploy:
build:
runs-on: windows-latest
permissions:
id-token: write #This is required for requesting the JWT
contents: read #This is required for actions/checkout
contents: read

steps:
- name: 'Checkout GitHub Action'
Expand All @@ -35,6 +40,30 @@ jobs:
pushd './${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }}'
dotnet build --configuration Release --output ./output
popd

- name: 'Upload build artifacts'
uses: actions/upload-artifact@v4
with:
name: build-output
path: ${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }}/output

deploy:
runs-on: windows-latest
needs: build
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
permissions:
id-token: write #This is required for requesting the JWT
contents: read #This is required for actions/checkout

steps:
- name: 'Checkout GitHub Action'
uses: actions/checkout@v4

- name: 'Download build artifacts'
uses: actions/download-artifact@v4
with:
name: build-output
path: ${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }}/output

- name: Login to Azure
uses: azure/login@v2
Expand Down
49 changes: 22 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,35 @@
## Scope
This Function application implements the AgGateway Traceability API particularly focused on
- Traceability Resource Units (TRU) which represents the quantity and quality measurements of an item in a container at a point in time
- Critical Tracking Events (CTE) particularly focused on the new Transfer Event where a TRU is transferred from one container into another container using a device resource, and potential having a remaining amount. Other CTEs are historically supported including the Transport Event, Change of Custoday Event and the Change of Ownership Event. Another new event is the Identification Event, which is the act of identification and provision of a unique identifier to record the event, item, device, container, etc.
- Critical Tracking Events (CTE) particularly focused on the new Transfer Event where a TRU is transferred from one container into another container using a device resource, and potential having a remaining amount. Other CTEs are historically supported including the Transport Event, Change of Custody Event and the Change of Ownership Event. Another new event is the Identification Event, which is the act of identification and provision of a unique identifier to record the event, item, device, container, etc.
- Key Data Elements including the shipment id, dock door id, trailer id, container id, field id, farmer id, farmer id, GLN, GTIN for product, etc. KDEs are most often identifiers and not measurements or observations. They help with the linkage of TRUs and CTEs.

## Endpoints
The endpoints are defined by the OpenAPI specification generated from the NIST Score tool, and developed by the AgGateway Traceability Work Group.

### Critical Tracking Event
POST /traceability/V1/critical-tracking-event
POST /traceability/V1/critical-tracking-event-list
GET /traceability/V1/critical-tracking-event-list
GET /traceability/V1/critical-tracking-event/{id}
PATCH /traceability/V1/critical-tracking-event/{id}
POST /traceability/V2/critical-tracking-event
POST /traceability/V2/critical-tracking-event-list
GET /traceability/V2/critical-tracking-event-list
GET /traceability/V2/critical-tracking-event/{id}
PATCH /traceability/V2/critical-tracking-event/{id}

### Field Operations
POST /traceability/V1/operation
PATCH /traceability/V1/operation/{id}
POST /traceability/V1/operator-party
GET /traceability/V1/container/{id}
GET /traceability/V1/container-list
GET /traceability/V1/device-resource-list
GET /traceability/V1/field/{id}
GET /traceability/V1/field-list
POST /traceability/V2/operation
PATCH /traceability/V2/operation/{id}
POST /traceability/V2/operator-party
GET /traceability/V2/container/{id}
GET /traceability/V2/container-list
GET /traceability/V2/device-resource-list
GET /traceability/V2/field/{id}
GET /traceability/V2/field-list

### Traceable Resource Unit
POST /traceability/V1/traceable-resource-unit
GET /traceability/V1/traceable-resource-unit/{id}
PATCH /traceability/V1/traceable-resource-unit/{id}
GET /traceability/V1/traceable-resource-unit-list
PATCH /traceability/traceable-resource-unit/{id}/V1/container-state

# Co-Pilot Prompt
Complete this function app by adding an HttpTrigger implementation of all the endpoints specified in the OpenAPI yml file in this project, ensuring that parameters are properly handled as specified with the endpoint. Each JSON payload in either the request body or response will reference the appropriate 'type' specified in the Model folder as related to the resource definition in the endpoint path, with a conversion from the '-' dash notation to the UpperCamelCase notation defined in the Model. All JSON will be persisted in the Cosmos DB. All PATCH operations will implement the upsert pattern, by retrieving the JSON by id or uuid, applying the Newtonsoft JSONPatch method already created in the helper class in this project, then replacing the JSON in the Cosmos DB container.

Sure, but use the existing workspace, the existing CosmosNoSQLAdapter.cs capabilities rewriting if necessary, and retain the OpenAPI yml file in the existing yml folder. Ensure the isolidate worker model is used, as in the existing function-app-traceability-api.cs files. Remove the function-app-traceability-api.cs file after migrating to CriticalTrackingEventListFunction.cs


Define the next steps to add new Agentic capabilities to the traceability capabilities defined in this OpenAPI yml. Determine if the exist function app can be extended (ideal) or whether it is necessary to create a new agent app. Ensure the the necessary MCP server protocols are able to interoperate with the OpenAPI specification in this yml. Define a mapping from the OpenAPI yml description keywords to that needed in the agent registry.
POST /traceability/V2/traceable-resource-unit
GET /traceability/V2/traceable-resource-unit/{id}
PATCH /traceability/V2/traceable-resource-unit/{id}
GET /traceability/V2/traceable-resource-unit-list
PATCH /traceability/V2/traceable-resource-unit/{id}/container-state

## Planting Operations Scenario
The planting operations sequence diagram shows which Traceability API V2 endpoints are used for this scenario. This involves logging into an application, loading the planter box from a seed tender (transfer event of TRUs), planting the seed into the field (transfer event of TRUs), and iterating until the field is completely. There could also be another scenario focused on positioning the seed tender properly for the refilling operations, knowing the points on the map wehere this occurred (transport event of the TRUs). In these scenarios, the container will likely have a remaining amount as it is not convenient to move the seed tender to the middle of the field.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ database DataPlatform

== log in ==
Farmer->MobileApp: log in
MobileApp -> MobileLocalAPI: POST /traceability/V2/operator-party
MobileApp -> DataPlatform: authenticate
MobileApp -> MobileLocalAPI: POST /traceability/V2/operator-party

== field selection; on-line ==
Farmer->MobileApp: retrieve my fields
Expand Down
17 changes: 5 additions & 12 deletions src/Functions/ContainerFunction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Newtonsoft.Json;
using IO.Swagger.Models;
using TraceabilityAPI.Cosmos.IO;
using TraceabilityAPI.Services;
using Microsoft.Azure.Cosmos;
using System.Net;

Expand Down Expand Up @@ -34,9 +35,7 @@ public async Task<HttpResponseData> GetById(
var item = await adapter.ReadDocument<IO.Swagger.Models.Container>(id, id, _logger);
if (item == null)
{
var notFound = req.CreateResponse(HttpStatusCode.NotFound);
await notFound.WriteStringAsync($"Item {id} not found.");
return notFound;
return await RfcProblemDetailsHelper.CreateProblemResponse(req, HttpStatusCode.NotFound, "Not Found", $"Item {id} not found.", null);
}

var ok = req.CreateResponse(HttpStatusCode.OK);
Expand All @@ -45,16 +44,12 @@ public async Task<HttpResponseData> GetById(
}
catch (Microsoft.Azure.Cosmos.CosmosException ce) when (ce.StatusCode == HttpStatusCode.NotFound)
{
var notFound = req.CreateResponse(HttpStatusCode.NotFound);
await notFound.WriteStringAsync($"Item {id} not found.");
return notFound;
return await RfcProblemDetailsHelper.CreateProblemResponse(req, HttpStatusCode.NotFound, "Not Found", $"Item {id} not found.", null);
}
catch (System.Exception ex)
{
_logger.LogError(ex, "Error reading container");
var err = req.CreateResponse(HttpStatusCode.InternalServerError);
await err.WriteStringAsync("Error reading container.");
return err;
return await RfcProblemDetailsHelper.CreateProblemResponse(req, HttpStatusCode.InternalServerError, "Internal Server Error", "Error reading container.", null);
}
}

Expand All @@ -75,9 +70,7 @@ public async Task<HttpResponseData> GetList(
catch (System.Exception ex)
{
_logger.LogError(ex, "Error querying containers");
var err = req.CreateResponse(HttpStatusCode.InternalServerError);
await err.WriteStringAsync("Error retrieving container list.");
return err;
return await RfcProblemDetailsHelper.CreateProblemResponse(req, HttpStatusCode.InternalServerError, "Internal Server Error", "Error retrieving container list.", null);
}
}
}
29 changes: 8 additions & 21 deletions src/Functions/CriticalTrackingEventByIdFunction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Newtonsoft.Json.Linq;
using IO.Swagger.Models;
using TraceabilityAPI.Cosmos.IO;
using TraceabilityAPI.Services;
using Microsoft.Azure.Cosmos;
using System.Net;

Expand Down Expand Up @@ -45,16 +46,12 @@ public async Task<HttpResponseData> GetById(
catch (System.Exception ex)
{
_logger.LogError(ex, "Error reading document");
var err = req.CreateResponse(HttpStatusCode.InternalServerError);
await err.WriteStringAsync("Error reading document.");
return err;
return await RfcProblemDetailsHelper.CreateProblemResponse(req, HttpStatusCode.InternalServerError, "Internal Server Error", "Error reading document.", null);
}

if (item == null)
{
var notFound = req.CreateResponse(HttpStatusCode.NotFound);
await notFound.WriteStringAsync($"Item {id} not found.");
return notFound;
return await RfcProblemDetailsHelper.CreateProblemResponse(req, HttpStatusCode.NotFound, "Not Found", $"Item {id} not found.", null);
}

var ok = req.CreateResponse(HttpStatusCode.OK);
Expand All @@ -70,9 +67,7 @@ public async Task<HttpResponseData> PatchById(
string body = await new StreamReader(req.Body).ReadToEndAsync();
if (string.IsNullOrWhiteSpace(body))
{
var bad = req.CreateResponse(HttpStatusCode.BadRequest);
await bad.WriteStringAsync("Request body is required.");
return bad;
return await RfcProblemDetailsHelper.CreateProblemResponse(req, HttpStatusCode.BadRequest, "Bad Request", "Request body is required.", null);
}

JObject patchPayload;
Expand All @@ -82,9 +77,7 @@ public async Task<HttpResponseData> PatchById(
}
catch (JsonException)
{
var bad = req.CreateResponse(HttpStatusCode.BadRequest);
await bad.WriteStringAsync("Invalid JSON payload.");
return bad;
return await RfcProblemDetailsHelper.CreateProblemResponse(req, HttpStatusCode.BadRequest, "Bad Request", "Invalid JSON payload.", null);
}

CriticalTrackingEventList existing = null;
Expand All @@ -99,9 +92,7 @@ public async Task<HttpResponseData> PatchById(
catch (System.Exception ex)
{
_logger.LogError(ex, "Error reading document for patch");
var err = req.CreateResponse(HttpStatusCode.InternalServerError);
await err.WriteStringAsync("Error reading existing document.");
return err;
return await RfcProblemDetailsHelper.CreateProblemResponse(req, HttpStatusCode.InternalServerError, "Internal Server Error", "Error reading existing document.", null);
}

CriticalTrackingEventList toUpsert;
Expand All @@ -121,17 +112,13 @@ public async Task<HttpResponseData> PatchById(
if (status != HttpStatusCode.Created && status != HttpStatusCode.OK && status != HttpStatusCode.NoContent)
{
_logger.LogError("Upsert returned non-success status: {Status}", status);
var err = req.CreateResponse(HttpStatusCode.InternalServerError);
await err.WriteStringAsync("Failed to persist patched object.");
return err;
return await RfcProblemDetailsHelper.CreateProblemResponse(req, HttpStatusCode.InternalServerError, "Internal Server Error", "Failed to persist patched object.", null);
}
}
catch (System.Exception ex)
{
_logger.LogError(ex, "Failed to upsert patched object");
var err = req.CreateResponse(HttpStatusCode.InternalServerError);
await err.WriteStringAsync("Failed to persist patched object.");
return err;
return await RfcProblemDetailsHelper.CreateProblemResponse(req, HttpStatusCode.InternalServerError, "Internal Server Error", "Failed to persist patched object.", null);
}

var resp = req.CreateResponse(HttpStatusCode.OK);
Expand Down
33 changes: 9 additions & 24 deletions src/Functions/CriticalTrackingEventFunction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Newtonsoft.Json.Linq;
using IO.Swagger.Models;
using TraceabilityAPI.Cosmos.IO;
using TraceabilityAPI.Services;
using Microsoft.Azure.Cosmos;
using System.Net;

Expand All @@ -32,9 +33,7 @@ public async Task<HttpResponseData> PostCriticalTrackingEvent(
var body = await new StreamReader(req.Body).ReadToEndAsync();
if (string.IsNullOrWhiteSpace(body))
{
var bad = req.CreateResponse(HttpStatusCode.BadRequest);
await bad.WriteStringAsync("Request body is required.");
return bad;
return await RfcProblemDetailsHelper.CreateProblemResponse(req, HttpStatusCode.BadRequest, "Bad Request", "Request body is required.", null);
}

CriticalTrackingEventList? payload;
Expand All @@ -45,16 +44,12 @@ public async Task<HttpResponseData> PostCriticalTrackingEvent(
catch (JsonException ex)
{
_logger.LogWarning(ex, "Payload deserialization failed");
var bad = req.CreateResponse(HttpStatusCode.BadRequest);
await bad.WriteStringAsync("Invalid JSON payload.");
return bad;
return await RfcProblemDetailsHelper.CreateProblemResponse(req, HttpStatusCode.BadRequest, "Bad Request", "Invalid JSON payload.", null);
}

if (payload is null)
{
var bad = req.CreateResponse(HttpStatusCode.BadRequest);
await bad.WriteStringAsync("Invalid payload for CriticalTrackingEventList.");
return bad;
return await RfcProblemDetailsHelper.CreateProblemResponse(req, HttpStatusCode.BadRequest, "Bad Request", "Invalid payload for CriticalTrackingEventList.", null);
}

var containerName = Environment.GetEnvironmentVariable("COSMOS_CONTAINER_CTE") ?? "critical-tracking-event";
Expand All @@ -67,17 +62,13 @@ public async Task<HttpResponseData> PostCriticalTrackingEvent(
if (status != HttpStatusCode.Created && status != HttpStatusCode.OK && status != HttpStatusCode.NoContent)
{
_logger.LogError("Cosmos write returned non-success status: {Status}", status);
var err = req.CreateResponse(HttpStatusCode.InternalServerError);
await err.WriteStringAsync("Failed to persist payload.");
return err;
return await RfcProblemDetailsHelper.CreateProblemResponse(req, HttpStatusCode.InternalServerError, "Internal Server Error", "Failed to persist payload.", null);
}
}
catch (System.Exception ex)
{
_logger.LogError(ex, "Failed to persist critical tracking event");
var err = req.CreateResponse(HttpStatusCode.InternalServerError);
await err.WriteStringAsync("Failed to persist payload.");
return err;
return await RfcProblemDetailsHelper.CreateProblemResponse(req, HttpStatusCode.InternalServerError, "Internal Server Error", "Failed to persist payload.", null);
}

var response = req.CreateResponse(HttpStatusCode.Created);
Expand Down Expand Up @@ -111,9 +102,7 @@ public async Task<HttpResponseData> PostCriticalTrackingEventList(

if (payload == null)
{
var bad = req.CreateResponse(HttpStatusCode.BadRequest);
await bad.WriteStringAsync("Invalid payload for CriticalTrackingEventList.");
return bad;
return await RfcProblemDetailsHelper.CreateProblemResponse(req, HttpStatusCode.BadRequest, "Bad Request", "Invalid payload for CriticalTrackingEventList.", null);
}

var containerName = Environment.GetEnvironmentVariable("COSMOS_CONTAINER_CTE_LIST") ?? "critical-tracking-event-list";
Expand All @@ -125,17 +114,13 @@ public async Task<HttpResponseData> PostCriticalTrackingEventList(
if (status != HttpStatusCode.Created && status != HttpStatusCode.OK && status != HttpStatusCode.NoContent)
{
_logger.LogError("Cosmos write returned non-success status: {Status}", status);
var err = req.CreateResponse(HttpStatusCode.InternalServerError);
await err.WriteStringAsync("Failed to persist payload.");
return err;
return await RfcProblemDetailsHelper.CreateProblemResponse(req, HttpStatusCode.InternalServerError, "Internal Server Error", "Failed to persist payload.", null);
}
}
catch (System.Exception ex)
{
_logger.LogError(ex, "Failed to persist critical tracking event list");
var err = req.CreateResponse(HttpStatusCode.InternalServerError);
await err.WriteStringAsync("Failed to persist payload.");
return err;
return await RfcProblemDetailsHelper.CreateProblemResponse(req, HttpStatusCode.InternalServerError, "Internal Server Error", "Failed to persist payload.", null);
}

var response = req.CreateResponse(HttpStatusCode.Created);
Expand Down
5 changes: 2 additions & 3 deletions src/Functions/CriticalTrackingEventListFunction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Newtonsoft.Json;
using IO.Swagger.Models;
using TraceabilityAPI.Cosmos.IO;
using TraceabilityAPI.Services;
using Microsoft.Azure.Cosmos;
using System.Net;

Expand Down Expand Up @@ -38,9 +39,7 @@ public async Task<HttpResponseData> GetList(
catch (System.Exception ex)
{
_logger.LogError(ex, "Error querying critical tracking event list");
var err = req.CreateResponse(HttpStatusCode.InternalServerError);
await err.WriteStringAsync("Error retrieving list.");
return err;
return await RfcProblemDetailsHelper.CreateProblemResponse(req, HttpStatusCode.InternalServerError, "Internal Server Error", "Error retrieving list.", null);
}
}
}
Loading
Loading