diff --git a/Directory.Packages.props b/Directory.Packages.props
index 7f240b2..6982489 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -5,16 +5,16 @@
-
+
-
-
-
-
-
+
+
+
+
+
@@ -22,5 +22,6 @@
+
\ No newline at end of file
diff --git a/MobileConfiguration/DataTransferObjects/Configuration.cs b/MobileConfiguration/DataTransferObjects/Configuration.cs
index b1ae582..c89a7dd 100644
--- a/MobileConfiguration/DataTransferObjects/Configuration.cs
+++ b/MobileConfiguration/DataTransferObjects/Configuration.cs
@@ -1,18 +1,28 @@
-namespace MobileConfiguration.DataTransferObjects;
+using System.ComponentModel;
+namespace MobileConfiguration.DataTransferObjects;
+
+[Description("Configuration settings associated with a Transaction Mobile device.")]
public class Configuration
{
+ [Description("Client identifier used by the mobile device.")]
public string ClientId { get; set; }
+ [Description("Client secret used by the mobile device. Treat this value as sensitive.")]
public string ClientSecret { get; set; }
+ [Description("Identifier of the mobile device.")]
public string DeviceIdentifier { get; set; }
+ [Description("Whether the mobile device should install updates automatically.")]
public bool EnableAutoUpdates { get; set; }
+ [Description("Service URI values available to the mobile device.")]
public List HostAddresses { get; set; }
+ [Description("Unique identifier for this configuration.")]
public string Id { get; set; }
+ [Description("Minimum severity level written by the mobile client. Enum values are numeric in JSON.")]
public LoggingLevel LogLevel { get; set; }
}
\ No newline at end of file
diff --git a/MobileConfiguration/DataTransferObjects/ConfigurationResponse.cs b/MobileConfiguration/DataTransferObjects/ConfigurationResponse.cs
index 4640574..87bc1ca 100644
--- a/MobileConfiguration/DataTransferObjects/ConfigurationResponse.cs
+++ b/MobileConfiguration/DataTransferObjects/ConfigurationResponse.cs
@@ -1,17 +1,27 @@
-namespace MobileConfiguration.DataTransferObjects;
+using System.ComponentModel;
+namespace MobileConfiguration.DataTransferObjects;
+
+[Description("Configuration returned for a Transaction Mobile device. Includes the client secret.")]
public class ConfigurationResponse
{
+ [Description("Client identifier used by the mobile device.")]
public string ClientId { get; set; }
+ [Description("Client secret used by the mobile device. Treat this value as sensitive.")]
public string ClientSecret { get; set; }
+ [Description("Identifier of the mobile device.")]
public string DeviceIdentifier { get; set; }
+ [Description("Whether the mobile device should install updates automatically.")]
public bool EnableAutoUpdates { get; set; }
+ [Description("Service URI values available to the mobile device.")]
public List HostAddresses { get; set; }
+ [Description("Unique identifier for this configuration.")]
public string Id { get; set; }
+ [Description("Minimum severity level written by the mobile client. Enum values are numeric in JSON.")]
public LoggingLevel LogLevel { get; set; }
}
\ No newline at end of file
diff --git a/MobileConfiguration/DataTransferObjects/HostAddress.cs b/MobileConfiguration/DataTransferObjects/HostAddress.cs
index 17e8c65..991d2a5 100644
--- a/MobileConfiguration/DataTransferObjects/HostAddress.cs
+++ b/MobileConfiguration/DataTransferObjects/HostAddress.cs
@@ -1,8 +1,13 @@
-namespace MobileConfiguration.DataTransferObjects;
+using System.ComponentModel;
+namespace MobileConfiguration.DataTransferObjects;
+
+[Description("A service type and the URI where the mobile client can reach it.")]
public class HostAddress
{
+ [Description("Service represented by this address. Enum values are numeric in JSON.")]
public ServiceType ServiceType { get; set; }
+ [Description("URI for the service endpoint.")]
public string Uri { get; set; }
}
\ No newline at end of file
diff --git a/MobileConfiguration/DataTransferObjects/LogMessage.cs b/MobileConfiguration/DataTransferObjects/LogMessage.cs
index e3d603c..fea43ca 100644
--- a/MobileConfiguration/DataTransferObjects/LogMessage.cs
+++ b/MobileConfiguration/DataTransferObjects/LogMessage.cs
@@ -1,8 +1,14 @@
-namespace MobileConfiguration.DataTransferObjects;
+using System.ComponentModel;
+namespace MobileConfiguration.DataTransferObjects;
+
+[Description("A log event submitted by a mobile client.")]
public class LogMessage
{
+ [Description("Timestamp associated with the log event.")]
public DateTime EntryDateTime { get; set; }
+ [Description("Log severity label supplied by the client.")]
public String LogLevel { get; set; }
+ [Description("Text of the log event.")]
public String Message { get; set; }
}
\ No newline at end of file
diff --git a/MobileConfiguration/MobileConfiguration.csproj b/MobileConfiguration/MobileConfiguration.csproj
index 135f365..33befbf 100644
--- a/MobileConfiguration/MobileConfiguration.csproj
+++ b/MobileConfiguration/MobileConfiguration.csproj
@@ -36,6 +36,7 @@
+
diff --git a/MobileConfiguration/OpenApi/MobileConfigurationExamplesOperationTransformer.cs b/MobileConfiguration/OpenApi/MobileConfigurationExamplesOperationTransformer.cs
new file mode 100644
index 0000000..43e49b8
--- /dev/null
+++ b/MobileConfiguration/OpenApi/MobileConfigurationExamplesOperationTransformer.cs
@@ -0,0 +1,119 @@
+using System.Text.Json.Nodes;
+using Microsoft.AspNetCore.OpenApi;
+using Microsoft.OpenApi;
+
+namespace MobileConfiguration.OpenApi;
+
+///
+/// Adds representative request and response payloads to the generated OpenAPI document.
+///
+internal sealed class MobileConfigurationExamplesOperationTransformer : IOpenApiOperationTransformer
+{
+ private const string ConfigurationExample = """
+ {
+ "clientId": "mobile-client",
+ "clientSecret": "example-secret",
+ "deviceIdentifier": "device-123",
+ "enableAutoUpdates": true,
+ "hostAddresses": [
+ { "serviceType": 2, "uri": "https://transactions.example.test" }
+ ],
+ "id": "device-123",
+ "logLevel": 3
+ }
+ """;
+
+ private const string ConfigurationResponseExample = """
+ {
+ "clientId": "mobile-client",
+ "clientSecret": "example-secret",
+ "deviceIdentifier": "device-123",
+ "enableAutoUpdates": true,
+ "hostAddresses": [
+ { "serviceType": 2, "uri": "https://transactions.example.test" }
+ ],
+ "id": "device-123",
+ "logLevel": 3
+ }
+ """;
+
+ private const string CreateSuccessExample = """
+ {
+ "status": 0,
+ "isSuccess": true,
+ "isFailed": false,
+ "message": "Configuration created successfully.",
+ "errors": []
+ }
+ """;
+
+ private const string CreateFailureExample = """
+ {
+ "status": 6,
+ "isSuccess": false,
+ "isFailed": true,
+ "message": "Failed to create configuration: persistence failed.",
+ "errors": []
+ }
+ """;
+
+ private const string LogMessagesExample = """
+ [
+ {
+ "entryDateTime": "2026-10-21 09:30:00",
+ "logLevel": "Information",
+ "message": "Mobile client started"
+ }
+ ]
+ """;
+
+ public Task TransformAsync(OpenApiOperation operation,
+ OpenApiOperationTransformerContext context,
+ CancellationToken cancellationToken)
+ {
+ string path = context.Description.RelativePath?.Trim('/') ?? string.Empty;
+ string? requestExample = (context.Description.HttpMethod, path) switch
+ {
+ ("POST", "api/transactionmobile/configuration") => ConfigurationExample,
+ ("PUT", "api/transactionmobile/configuration/{id}") => ConfigurationExample,
+ ("POST", "api/transactionmobile/logging") => LogMessagesExample,
+ _ => null
+ };
+
+ if (requestExample is not null
+ && operation.RequestBody is OpenApiRequestBody requestBody
+ && requestBody.Content is not null
+ && requestBody.Content.TryGetValue("application/json", out OpenApiMediaType? requestMediaType))
+ {
+ requestMediaType.Example = JsonNode.Parse(requestExample);
+ }
+
+ string? successResponseExample = (context.Description.HttpMethod, path) switch
+ {
+ ("POST", "api/transactionmobile/configuration") => CreateSuccessExample,
+ ("GET", "api/transactionmobile/configuration/{id}") => ConfigurationResponseExample,
+ _ => null
+ };
+
+ SetResponseExample(operation, "200", successResponseExample);
+
+ if (context.Description.HttpMethod == "POST" && path == "api/transactionmobile/configuration")
+ {
+ SetResponseExample(operation, "400", CreateFailureExample);
+ }
+
+ return Task.CompletedTask;
+ }
+
+ private static void SetResponseExample(OpenApiOperation operation, string statusCode, string? example)
+ {
+ if (example is not null
+ && operation.Responses is not null
+ && operation.Responses.TryGetValue(statusCode, out IOpenApiResponse? response)
+ && response.Content is not null
+ && response.Content.TryGetValue("application/json", out OpenApiMediaType? mediaType))
+ {
+ mediaType.Example = JsonNode.Parse(example);
+ }
+ }
+}
diff --git a/MobileConfiguration/Program.cs b/MobileConfiguration/Program.cs
index 766c6e1..40630ff 100644
--- a/MobileConfiguration/Program.cs
+++ b/MobileConfiguration/Program.cs
@@ -3,6 +3,7 @@
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.EntityFrameworkCore;
using MobileConfiguration.Database;
+using MobileConfiguration.DataTransferObjects;
using MobileConfiguration.Repository;
using NLog;
using NLog.Extensions.Logging;
@@ -17,6 +18,9 @@
using System.Reflection;
using System.Text.Json;
using Microsoft.Extensions.Diagnostics.HealthChecks;
+using SimpleResults;
+using TransactionProcessing.ApiDefaults;
+using MobileConfiguration.OpenApi;
using ILogger = Microsoft.Extensions.Logging.ILogger;
using LogLevel = Microsoft.Extensions.Logging.LogLevel;
@@ -154,6 +158,15 @@
builder.Services.AddHealthMonitoringRegistration(configuration);
+builder.Services.AddApiDefaults(options => {
+ options.Title = "Mobile Configuration";
+ options.Description = "API to maintain all aspects of mobile device configuration";
+});
+builder.Services.AddOpenApi(options =>
+{
+ options.AddOperationTransformer(new MobileConfigurationExamplesOperationTransformer());
+});
+
var app = builder.Build();
var serialiser = app.Services.GetRequiredService();
@@ -161,9 +174,7 @@
app.UseMiddleware();
-app.UseSwagger();
-app.UseSwaggerUI();
-
+app.UseApiDefaults();
// Configure the HTTP request pipeline.
app.UseAuthorization();
@@ -173,24 +184,60 @@
// Minimal API endpoints (handler pattern)
-app.MapPost("/api/TransactionMobileConfiguration", MobileConfiguration.Handlers.TransactionMobileConfigurationHandler.PostConfiguration);
-app.MapGet("/api/TransactionMobileConfiguration/{id}", MobileConfiguration.Handlers.TransactionMobileConfigurationHandler.GetConfiguration);
-app.MapPut("/api/TransactionMobileConfiguration/{id}", MobileConfiguration.Handlers.TransactionMobileConfigurationHandler.PutConfiguration);
-
-app.MapPost("/api/TransactionMobileLogging", MobileConfiguration.Handlers.TransactionMobileLoggingHandler.PostLogging);
+String configBaseRoute = "/api/transactionmobile/configuration";
+
+app.MapPost(configBaseRoute, MobileConfiguration.Handlers.TransactionMobileConfigurationHandler.PostConfiguration)
+ .WithName("CreateTransactionMobileConfiguration")
+ .WithTags("Mobile Configuration")
+ .WithSummary("Create a mobile configuration")
+ .WithDescription("Creates a Transaction Mobile configuration and returns the persistence result. A failed persistence operation is returned as 400 Bad Request.")
+ .Accepts("application/json")
+ .Produces(StatusCodes.Status200OK)
+ .Produces(StatusCodes.Status400BadRequest);
+app.MapGet($"{configBaseRoute}/{{id}}", MobileConfiguration.Handlers.TransactionMobileConfigurationHandler.GetConfiguration)
+ .WithName("GetTransactionMobileConfiguration")
+ .WithTags("Mobile Configuration")
+ .WithSummary("Get a mobile configuration")
+ .WithDescription("Returns the Transaction Mobile configuration for the supplied identifier. The response contains the configured client secret.")
+ .Produces(StatusCodes.Status200OK);
+app.MapPut($"{configBaseRoute}/{{id}}", MobileConfiguration.Handlers.TransactionMobileConfigurationHandler.PutConfiguration)
+ .WithName("UpsertTransactionMobileConfiguration")
+ .WithTags("Mobile Configuration")
+ .WithSummary("Create or update a mobile configuration")
+ .WithDescription("Updates the Transaction Mobile configuration for the supplied identifier, or creates it if it does not exist. For an existing record, the path identifier selects the record.")
+ .Accepts("application/json")
+ .Produces(StatusCodes.Status200OK);
+
+String loggingBaseRoute = "/api/transactionmobile/logging";
+
+app.MapPost(loggingBaseRoute, MobileConfiguration.Handlers.TransactionMobileLoggingHandler.PostLogging)
+ .WithName("SubmitTransactionMobileLogs")
+ .WithTags("Mobile Logging")
+ .WithSummary("Submit mobile log messages")
+ .WithDescription("Writes the submitted log messages to the service logger. The messages are not stored in the configuration database.")
+ .Accepts>("application/json")
+ .Produces(StatusCodes.Status200OK);
app.MapHealthChecks("health",
new HealthCheckOptions
{
Predicate = _ => true,
ResponseWriter = Shared.HealthChecks.HealthCheckMiddleware.WriteResponse
- });
+ })
+ .WithName("HealthCheck")
+ .WithTags("Health")
+ .WithSummary("Check service and dependency health")
+ .WithDescription("Returns health-check details for the service and its configured dependencies.");
app.MapHealthChecks("healthui",
new HealthCheckOptions
{
Predicate = _ => true,
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
- });
+ })
+ .WithName("HealthCheckUi")
+ .WithTags("Health")
+ .WithSummary("Get health details for the monitoring UI")
+ .WithDescription("Returns health-check data formatted for the health monitoring UI.");
InitializeDatabase(app).Wait(CancellationToken.None);
@@ -222,4 +269,4 @@ public static void ConfigureMinimalApi(JsonSerializerOptions serializerOptions)
serializerOptions.WriteIndented = defaultOptions.WriteIndented;
serializerOptions.Converters.Add(new DateTimeSpaceConverter());
}
-}
\ No newline at end of file
+}
diff --git a/README.md b/README.md
index 8aae717..8263a9b 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,3 @@
-# MobileConfiguration
\ No newline at end of file
+# MobileConfiguration
+
+Open Scalar at `/scalar` for the interactive, generated API reference. See [the API and DTO reference](docs/api.md) for endpoint routes, request and response shapes, enum values, and examples.
diff --git a/docs/api.md b/docs/api.md
new file mode 100644
index 0000000..43e71e3
--- /dev/null
+++ b/docs/api.md
@@ -0,0 +1,207 @@
+# Mobile Configuration API
+
+This document describes the HTTP endpoints and JSON data transfer objects exposed by the service. The service listens on port `9200` by default. The interactive Scalar reference is available at `/scalar`; it is generated from the OpenAPI metadata on the endpoints and DTO properties.
+
+## Conventions
+
+- Request and response bodies use JSON. Property names are camel case (for example, `clientId`).
+- Enum values are represented as JSON integers, using the numeric values shown below.
+- The API startup configures authorization middleware but does not declare an authentication scheme or endpoint-specific authorization policy. Any protection supplied by hosting infrastructure is outside this contract.
+- Configuration responses include `clientSecret`. Treat them as sensitive and protect the API and its traffic accordingly.
+- The endpoint behavior below reflects the handlers and repository implementation. Error response bodies are produced by shared exception handling and are not specified here.
+
+## Endpoints
+
+### Create a configuration
+
+`POST /api/transactionmobile/configuration`
+
+Creates a Transaction Mobile configuration.
+
+**Request body:** [`Configuration`](#configuration)
+
+**Responses:**
+
+- `200 OK` with the repository result when the create operation succeeds.
+- `400 Bad Request` with the repository result when persistence reports failure.
+
+Example request:
+
+```json
+{
+ "clientId": "mobile-client",
+ "clientSecret": "replace-with-secret",
+ "deviceIdentifier": "device-123",
+ "enableAutoUpdates": true,
+ "hostAddresses": [
+ { "serviceType": 2, "uri": "https://transactions.example.com" }
+ ],
+ "id": "device-123",
+ "logLevel": 3
+}
+```
+
+Example successful response (`200 OK`):
+
+```json
+{
+ "status": 0,
+ "isSuccess": true,
+ "isFailed": false,
+ "message": "Configuration created successfully.",
+ "errors": []
+}
+```
+
+Example persistence failure (`400 Bad Request`):
+
+```json
+{
+ "status": 6,
+ "isSuccess": false,
+ "isFailed": true,
+ "message": "Failed to create configuration: persistence failed.",
+ "errors": []
+}
+```
+
+### Get a configuration
+
+`GET /api/transactionmobile/configuration/{id}`
+
+Returns the Transaction Mobile configuration whose ID matches the path value.
+
+**Path parameters:**
+
+| Name | Type | Description |
+| --- | --- | --- |
+| `id` | string | Configuration identifier. |
+
+**Responses:**
+
+- `200 OK` with a [`ConfigurationResponse`](#configurationresponse) body.
+- If no matching configuration exists, the repository throws a not-found exception; the exact HTTP status and body depend on the shared exception handler.
+
+Example response:
+
+```json
+{
+ "clientId": "mobile-client",
+ "clientSecret": "example-secret",
+ "deviceIdentifier": "device-123",
+ "enableAutoUpdates": true,
+ "hostAddresses": [
+ { "serviceType": 2, "uri": "https://transactions.example.test" }
+ ],
+ "id": "device-123",
+ "logLevel": 3
+}
+```
+
+### Create or update a configuration
+
+`PUT /api/transactionmobile/configuration/{id}`
+
+Updates the configuration matching `id`, or creates one if it does not exist. The configuration type is Transaction Mobile.
+
+**Path parameters:**
+
+| Name | Type | Description |
+| --- | --- | --- |
+| `id` | string | Identifier of the configuration to update or create. |
+
+**Request body:** [`Configuration`](#configuration). The body `id` is used when a new record is created; for an existing record, the path `id` selects the record and the body ID is not used to change its key.
+
+**Responses:**
+
+- `200 OK` with an empty body after persistence completes.
+- If saving fails or no database is available, the shared exception handler determines the error response.
+
+### Submit mobile log messages
+
+`POST /api/transactionmobile/logging`
+
+Writes the submitted log-message list to the service logger. This endpoint does not persist the entries through the configuration database.
+
+**Request body:** array of [`LogMessage`](#logmessage) objects.
+
+Example request:
+
+```json
+[
+ {
+ "entryDateTime": "2026-10-21 09:30:00",
+ "logLevel": "Information",
+ "message": "Mobile client started"
+ }
+]
+```
+
+**Responses:** `200 OK` with an empty body.
+
+### Health checks
+
+- `GET /health` returns health-check details in the service's shared response format.
+- `GET /healthui` returns the Health Checks UI response format.
+
+## DTOs
+
+### Configuration
+
+| Property | JSON type | Description |
+| --- | --- | --- |
+| `clientId` | string | Client identifier. |
+| `clientSecret` | string | Client secret. Sensitive value. |
+| `deviceIdentifier` | string | Device identifier associated with the configuration. |
+| `enableAutoUpdates` | boolean | Whether automatic updates are enabled. |
+| `hostAddresses` | array of [`HostAddress`](#hostaddress) | Service endpoints available to the mobile client. |
+| `id` | string | Configuration identifier. |
+| `logLevel` | integer | [`LoggingLevel`](#logginglevel) value. |
+
+### ConfigurationResponse
+
+The response DTO returned by the GET endpoint. It has the same properties and types as [`Configuration`](#configuration), including `clientSecret`.
+
+### HostAddress
+
+| Property | JSON type | Description |
+| --- | --- | --- |
+| `serviceType` | integer | [`ServiceType`](#servicetype) value. |
+| `uri` | string | URI for the service. |
+
+### LogMessage
+
+| Property | JSON type | Description |
+| --- | --- | --- |
+| `entryDateTime` | string (date-time) | Timestamp associated with the log entry. |
+| `logLevel` | string | Caller-provided level label. |
+| `message` | string | Log message text. |
+
+## Enums
+
+### LoggingLevel
+
+| Name | Value |
+| --- | ---: |
+| `Fatal` | 0 |
+| `Error` | 1 |
+| `Warning` | 2 |
+| `Information` | 3 |
+| `Debug` | 4 |
+| `Trace` | 5 |
+
+### ServiceType
+
+| Name | Value |
+| --- | ---: |
+| `EstateManagement` | 0 |
+| `Security` | 1 |
+| `TransactionProcessorAcl` | 2 |
+| `VoucherManagementAcl` | 3 |
+
+## Known contract limitations
+
+- DTO properties are not annotated with validation rules, so this document does not promise field-level validation or required-field error details.
+- `PUT` updates the values on the existing record but does not change its identifier.
+- The GET response mapping currently preserves `TransactionProcessorAcl` host types; other stored host types map to `Security` in the response.
+- The create endpoint returns the shared repository result object. The OpenAPI schema and example describe its current serialized shape.