diff --git a/src/Altinn.App.Core/Altinn.App.Core.csproj b/src/Altinn.App.Core/Altinn.App.Core.csproj
index 94b3ad9364..1e6347348e 100644
--- a/src/Altinn.App.Core/Altinn.App.Core.csproj
+++ b/src/Altinn.App.Core/Altinn.App.Core.csproj
@@ -15,7 +15,6 @@
-
diff --git a/src/Altinn.App.Core/Configuration/DanSettings.cs b/src/Altinn.App.Core/Configuration/DanSettings.cs
new file mode 100644
index 0000000000..21d1224b9f
--- /dev/null
+++ b/src/Altinn.App.Core/Configuration/DanSettings.cs
@@ -0,0 +1,17 @@
+namespace Altinn.App.Core.Configuration;
+
+///
+/// Settings for DanClient
+///
+public class DanSettings
+{
+ ///
+ /// Base url for Dan API
+ ///
+ public required string BaseUrl { get; set; }
+
+ ///
+ /// Api subscription keys
+ ///
+ public required string SubscriptionKey { get; set; }
+}
diff --git a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs
index 0e4dc047f4..3c5b0b0c97 100644
--- a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs
+++ b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs
@@ -25,6 +25,7 @@
using Altinn.App.Core.Infrastructure.Clients.AccessManagement;
using Altinn.App.Core.Infrastructure.Clients.Authentication;
using Altinn.App.Core.Infrastructure.Clients.Authorization;
+using Altinn.App.Core.Infrastructure.Clients.Dan;
using Altinn.App.Core.Infrastructure.Clients.Events;
using Altinn.App.Core.Infrastructure.Clients.KeyVault;
using Altinn.App.Core.Infrastructure.Clients.Pdf;
@@ -36,6 +37,7 @@
using Altinn.App.Core.Internal.App;
using Altinn.App.Core.Internal.AppModel;
using Altinn.App.Core.Internal.Auth;
+using Altinn.App.Core.Internal.Dan;
using Altinn.App.Core.Internal.Data;
using Altinn.App.Core.Internal.Events;
using Altinn.App.Core.Internal.Expressions;
@@ -195,6 +197,12 @@ IWebHostEnvironment env
services.Configure(configuration.GetSection("AccessTokenSettings"));
services.Configure(configuration.GetSection(nameof(FrontEndSettings)));
services.Configure(configuration.GetSection(nameof(PdfGeneratorSettings)));
+ var danSettings = configuration.GetSection("DanClientSettings");
+ if (danSettings.Exists())
+ {
+ services.AddHttpClient();
+ services.Configure(danSettings);
+ }
services.AddRuntimeEnvironment();
if (env.IsDevelopment())
diff --git a/src/Altinn.App.Core/Implementation/PrefillSI.cs b/src/Altinn.App.Core/Implementation/PrefillSI.cs
index 7e5a1722d9..d3f0e513bd 100644
--- a/src/Altinn.App.Core/Implementation/PrefillSI.cs
+++ b/src/Altinn.App.Core/Implementation/PrefillSI.cs
@@ -3,6 +3,7 @@
using Altinn.App.Core.Features;
using Altinn.App.Core.Features.Auth;
using Altinn.App.Core.Internal.App;
+using Altinn.App.Core.Internal.Dan;
using Altinn.App.Core.Internal.Prefill;
using Altinn.App.Core.Internal.Registers;
using Altinn.Platform.Profile.Models;
@@ -20,10 +21,12 @@ public class PrefillSI : IPrefill
private readonly IAppResources _appResourcesService;
private readonly IRegisterClient _registerClient;
private readonly IAuthenticationContext _authenticationContext;
+ private readonly IDanClient? _danClient;
private readonly Telemetry? _telemetry;
private static readonly string _erKey = "ER";
private static readonly string _dsfKey = "DSF";
private static readonly string _userProfileKey = "UserProfile";
+ private static readonly string _danKey = "DAN";
private static readonly string _allowOverwriteKey = "allowOverwrite";
private bool _allowOverwrite = false;
@@ -34,12 +37,14 @@ public class PrefillSI : IPrefill
/// The app's resource service
/// The authentication context
/// The service provider
+ /// The Dan client
/// Telemetry for traces and metrics.
public PrefillSI(
ILogger logger,
IAppResources appResourcesService,
IAuthenticationContext authenticationContext,
IServiceProvider serviceProvider,
+ IDanClient? danClient = null,
Telemetry? telemetry = null
)
{
@@ -47,6 +52,7 @@ public PrefillSI(
_appResourcesService = appResourcesService;
_registerClient = serviceProvider.GetRequiredService();
_authenticationContext = authenticationContext;
+ _danClient = danClient;
_telemetry = telemetry;
}
@@ -206,6 +212,63 @@ when await systemUser.LoadDetails() is { } details && details.Party.PartyId == p
}
}
}
+
+ // Prefill from Dan
+ JToken? danConfiguration = prefillConfiguration.SelectToken(_danKey);
+ if (danConfiguration != null && _danClient != null)
+ {
+ var datasetList = danConfiguration.SelectToken("datasets");
+ if (datasetList != null)
+ {
+ foreach (var dataset in datasetList)
+ {
+ var datasetName = dataset.SelectToken("name")?.ToString();
+ var subject = !string.IsNullOrWhiteSpace(party.SSN) ? party.SSN : party.OrgNumber;
+
+ if (string.IsNullOrEmpty(subject))
+ {
+ _logger.LogError(
+ "Could not prefill from {DanKey}, no valid subject (SSN or OrgNumber) found for party",
+ _danKey
+ );
+ continue;
+ }
+
+ var fields = dataset.SelectToken("mappings");
+ if (fields != null)
+ {
+ var danPrefill = fields
+ .SelectMany(obj => obj.Children())
+ .ToDictionary(prop => prop.Name, prop => prop.Value.ToString());
+
+ if (datasetName != null)
+ {
+ var danDataset = await _danClient.GetDataset(datasetName, subject, fields.ToString());
+ if (danDataset.Count > 0)
+ {
+ JObject danJsonObject = JObject.FromObject(danDataset);
+ _logger.LogInformation($"Started prefill from {_danKey}");
+ LoopThroughDictionaryAndAssignValuesToDataModel(
+ SwapKeyValuesForPrefill(danPrefill),
+ danJsonObject,
+ dataModel
+ );
+ }
+ else
+ {
+ string errorMessage = $"Could not prefill from {_danKey}, data is not defined.";
+ _logger.LogError(errorMessage);
+ }
+ }
+ else
+ {
+ string errorMessage = $"Could not prefill from {_danKey}, dataset name is not defined.";
+ _logger.LogError(errorMessage);
+ }
+ }
+ }
+ }
+ }
}
///
diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Dan/DanClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Dan/DanClient.cs
new file mode 100644
index 0000000000..46c8e8c8ef
--- /dev/null
+++ b/src/Altinn.App.Core/Infrastructure/Clients/Dan/DanClient.cs
@@ -0,0 +1,117 @@
+using System.Text;
+using System.Text.Json;
+using Altinn.App.Core.Configuration;
+using Altinn.App.Core.Internal.Dan;
+using Microsoft.Extensions.Options;
+using Newtonsoft.Json;
+using JsonException = Newtonsoft.Json.JsonException;
+using JsonSerializer = System.Text.Json.JsonSerializer;
+
+namespace Altinn.App.Core.Infrastructure.Clients.Dan;
+
+///
+/// Client for interacting with the Dan API.
+///
+public class DanClient : IDanClient
+{
+ private readonly HttpClient _httpClient;
+ private readonly IOptions _settings;
+
+ ///
+ /// Constructor
+ ///
+ ///
+ ///
+ public DanClient(IHttpClientFactory factory, IOptions settings)
+ {
+ _httpClient = factory.CreateClient("DanClient");
+ _settings = settings;
+
+ _httpClient.DefaultRequestHeaders.Clear();
+ _httpClient.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", _settings.Value.SubscriptionKey);
+ _httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
+ _httpClient.BaseAddress = new Uri(settings.Value.BaseUrl);
+ }
+
+ ///
+ /// Returns dataset from Dan API.
+ ///
+ /// Dataset from Dan
+ /// Usually ssn or orgNumber
+ /// The fields we fetch from the api
+ ///
+ public async Task> GetDataset(string dataset, string subject, string fields)
+ {
+ var body = new { Subject = subject };
+ var myContent = JsonConvert.SerializeObject(body);
+
+ var fieldsToFill = GetQuery(fields);
+ if (fieldsToFill.Count == 0)
+ return new Dictionary();
+
+ var baseQuery = $"{fieldsToFill.First()} : {fieldsToFill.First()}";
+ foreach (var jsonKey in fieldsToFill.Skip(1))
+ {
+ //if there is more than one field to fetch, add it to the query
+ baseQuery += $",{jsonKey} : {jsonKey}";
+ }
+
+ //ensures that the query returns a list if endpoint returns a list and an object when endpoint returns a single object
+ var query = "[].{" + baseQuery + "}||{" + baseQuery + "}";
+ using (var content = new StringContent(myContent, Encoding.UTF8, "application/json"))
+ {
+ var result = await _httpClient.PostAsync(
+ $"directharvest/{dataset}?envelope=false&reuseToken=true&query={query}",
+ content
+ );
+
+ if (result.IsSuccessStatusCode)
+ {
+ Dictionary? dictionary;
+
+ var resultJson = await result.Content.ReadAsStringAsync();
+
+ dictionary = IsJsonArray(resultJson)
+ ? await ConvertListToDictionary(resultJson)
+ : JsonConvert.DeserializeObject>(resultJson);
+
+ return dictionary ?? new Dictionary();
+ }
+ }
+ return new Dictionary();
+ }
+
+ private static Task> ConvertListToDictionary(string jsonString)
+ {
+ var list = JsonConvert.DeserializeObject>>(jsonString);
+ if (list != null)
+ return Task.FromResult(
+ list.SelectMany(d => d)
+ .GroupBy(kvp => kvp.Key)
+ .ToDictionary(g => g.Key, g => string.Join(",", g.Select(x => x.Value)))
+ );
+
+ return Task.FromResult(new Dictionary());
+ }
+
+ private static bool IsJsonArray(string jsonString)
+ {
+ try
+ {
+ using var doc = JsonDocument.Parse(jsonString);
+ return doc.RootElement.ValueKind == JsonValueKind.Array;
+ }
+ catch (JsonException)
+ {
+ return false;
+ }
+ }
+
+ private static List GetQuery(string json)
+ {
+ var list = JsonSerializer.Deserialize>>(json);
+ if (list != null)
+ return list.Where(l => l.Count != 0).Select(l => l.Keys.First()).ToList();
+ return new List();
+ }
+}
diff --git a/src/Altinn.App.Core/Internal/Dan/IDanClient.cs b/src/Altinn.App.Core/Internal/Dan/IDanClient.cs
new file mode 100644
index 0000000000..fe0e6255e1
--- /dev/null
+++ b/src/Altinn.App.Core/Internal/Dan/IDanClient.cs
@@ -0,0 +1,16 @@
+namespace Altinn.App.Core.Internal.Dan;
+
+///
+/// DanClient interface
+///
+public interface IDanClient
+{
+ ///
+ /// Method for getting a selected dataset from Dan Api
+ ///
+ /// Name of the dataset
+ /// Usually ssn or OrgNumber
+ /// fields to fetch from endpoint
+ ///
+ public Task> GetDataset(string dataset, string subject, string fields);
+}
diff --git a/test/Altinn.App.Api.Tests/Program.cs b/test/Altinn.App.Api.Tests/Program.cs
index a2db05bc5d..b8f504c580 100644
--- a/test/Altinn.App.Api.Tests/Program.cs
+++ b/test/Altinn.App.Api.Tests/Program.cs
@@ -8,9 +8,11 @@
using Altinn.App.Core.Configuration;
using Altinn.App.Core.Features;
using Altinn.App.Core.Features.Cache;
+using Altinn.App.Core.Infrastructure.Clients.Dan;
using Altinn.App.Core.Internal.App;
using Altinn.App.Core.Internal.AppModel;
using Altinn.App.Core.Internal.Auth;
+using Altinn.App.Core.Internal.Dan;
using Altinn.App.Core.Internal.Data;
using Altinn.App.Core.Internal.Events;
using Altinn.App.Core.Internal.Instances;
@@ -67,6 +69,11 @@
builder.Services.Configure(settings => settings.DisableLocaltestValidation = true);
builder.Services.Configure(settings => settings.DisableAppConfigurationCache = true);
builder.Services.Configure(settings => settings.IsTest = true);
+builder.Services.Configure(settings =>
+{
+ settings.BaseUrl = "http://localhost:7071/v1/";
+ settings.SubscriptionKey = "test-subscription-key";
+});
builder.Configuration.GetSection("GeneralSettings:IsTest").Value = "true";
// AppConfigurationCache.Disable = true;
diff --git a/test/Altinn.App.Core.Tests/Implementation/PrefillSITest.cs b/test/Altinn.App.Core.Tests/Implementation/PrefillSITest.cs
index 37266defc2..204188eb69 100644
--- a/test/Altinn.App.Core.Tests/Implementation/PrefillSITest.cs
+++ b/test/Altinn.App.Core.Tests/Implementation/PrefillSITest.cs
@@ -1,7 +1,9 @@
using Altinn.App.Core.Features.Auth;
using Altinn.App.Core.Implementation;
using Altinn.App.Core.Internal.App;
+using Altinn.App.Core.Internal.Dan;
using Altinn.App.Core.Internal.Registers;
+using Altinn.Platform.Register.Models;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Moq;
@@ -13,6 +15,12 @@ public class PrefillTestDataModel
public TestPrefillFields? Prefill { get; set; }
}
+public class PrefillDanTestDataModel
+{
+ public string? Email { get; set; }
+ public string? OrganizationNumber { get; set; }
+}
+
public class TestPrefillFields
{
public string? EraSourceEnvironment { get; set; }
@@ -47,6 +55,7 @@ public async Task PrefillDataModel_AssignsValuesCorrectly()
var authenticationContextMock = new Mock();
var services = new ServiceCollection();
var registryClientMock = new Mock();
+ var danClientMock = new Mock();
services.AddSingleton(registryClientMock.Object);
await using var sp = services.BuildStrictServiceProvider();
@@ -54,7 +63,8 @@ public async Task PrefillDataModel_AssignsValuesCorrectly()
loggerMock.Object,
appResourcesMock.Object,
authenticationContextMock.Object,
- sp
+ sp,
+ danClientMock.Object
);
prefillToTest.PrefillDataModel(dataModel, externalPrefill, continueOnError: false);
@@ -68,4 +78,62 @@ public async Task PrefillDataModel_AssignsValuesCorrectly()
Assert.Equal("S'oderberg og Partners", dataModel.Prefill.YrkesskadeforsikringNavn);
Assert.Equal("2023-12-31T12:00:00.000+01:00", dataModel.Prefill.YrkesskadeforsikringGyldigTilDato);
}
+
+ [Fact]
+ public async Task PrefillDataModel_Should_Fill_With_Data_From_Dan()
+ {
+ // Arrange
+ var dataModel = new PrefillDanTestDataModel();
+
+ var loggerMock = new Mock>();
+ var appResourcesMock = new Mock();
+ var authenticationContextMock = new Mock();
+ var services = new ServiceCollection();
+ var registryClientMock = new Mock();
+ var danClientMock = new Mock();
+ services.AddSingleton(registryClientMock.Object);
+ await using var sp = services.BuildStrictServiceProvider();
+
+ var prefillToTest = new PrefillSI(
+ loggerMock.Object,
+ appResourcesMock.Object,
+ authenticationContextMock.Object,
+ sp,
+ danClientMock.Object
+ );
+
+ var modelName = "model";
+ var partyId = "1234";
+
+ // danData should match the data in the GetJsonConfig method
+ var danData = new Dictionary
+ {
+ { "BusinessAddressCity", "Email" },
+ { "SectorCode", "OrganizationNumber" },
+ };
+
+ var party = new Party() { PartyId = 1234, SSN = "12341234" };
+
+ appResourcesMock.Setup(ar => ar.GetPrefillJson(It.IsAny())).Returns(GetJsonConfig());
+ registryClientMock
+ .Setup(m => m.GetPartyUnchecked(It.IsAny(), It.IsAny()))
+ .ReturnsAsync(party);
+ danClientMock
+ .Setup(m => m.GetDataset(It.IsAny(), It.IsAny(), It.IsAny()))
+ .ReturnsAsync(danData);
+
+ //Act
+ await prefillToTest.PrefillDataModel(partyId, modelName, dataModel);
+
+ //Assert
+ danClientMock.Verify(
+ m => m.GetDataset(It.IsAny(), It.IsAny(), It.IsAny()),
+ Times.AtLeastOnce
+ );
+ }
+
+ private string GetJsonConfig()
+ {
+ return "{\n \"$schema\" : \"https://altinncdn.no/schemas/json/prefill/prefill.schema.v1.json\",\n \"allowOverwrite\" : true,\n \"DAN\" : {\n \"datasets\" : [ {\n \"name\" : \"UnitBasicInformation\",\n \"mappings\" : [ {\n \"BusinessAddressCity\" : \"Email\"\n }, {\n \"SectorCode\" : \"OrganizationNumber\"\n } ]\n } ]\n }\n}";
+ }
}
diff --git a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt
index befc64ef92..22766a75ac 100644
--- a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt
+++ b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt
@@ -51,6 +51,12 @@ namespace Altinn.App.Core.Configuration
public CacheSettings() { }
public int ProfileCacheLifetimeSeconds { get; set; }
}
+ public class DanSettings
+ {
+ public DanSettings() { }
+ public required string BaseUrl { get; set; }
+ public required string SubscriptionKey { get; set; }
+ }
public class FrontEndSettings : System.Collections.Generic.Dictionary
{
public FrontEndSettings() { }
@@ -2365,7 +2371,7 @@ namespace Altinn.App.Core.Implementation
}
public class PrefillSI : Altinn.App.Core.Internal.Prefill.IPrefill
{
- public PrefillSI(Microsoft.Extensions.Logging.ILogger logger, Altinn.App.Core.Internal.App.IAppResources appResourcesService, Altinn.App.Core.Features.Auth.IAuthenticationContext authenticationContext, System.IServiceProvider serviceProvider, Altinn.App.Core.Features.Telemetry? telemetry = null) { }
+ public PrefillSI(Microsoft.Extensions.Logging.ILogger logger, Altinn.App.Core.Internal.App.IAppResources appResourcesService, Altinn.App.Core.Features.Auth.IAuthenticationContext authenticationContext, System.IServiceProvider serviceProvider, Altinn.App.Core.Internal.Dan.IDanClient? danClient = null, Altinn.App.Core.Features.Telemetry? telemetry = null) { }
public void PrefillDataModel(object dataModel, System.Collections.Generic.Dictionary externalPrefill, bool continueOnError = false) { }
public System.Threading.Tasks.Task PrefillDataModel(string partyId, string dataModelName, object dataModel, System.Collections.Generic.Dictionary? externalPrefill = null) { }
}
@@ -2396,6 +2402,14 @@ namespace Altinn.App.Core.Infrastructure.Clients.Authorization
public System.Threading.Tasks.Task ValidateSelectedParty(int userId, int partyId) { }
}
}
+namespace Altinn.App.Core.Infrastructure.Clients.Dan
+{
+ public class DanClient : Altinn.App.Core.Internal.Dan.IDanClient
+ {
+ public DanClient(System.Net.Http.IHttpClientFactory factory, Microsoft.Extensions.Options.IOptions settings) { }
+ public System.Threading.Tasks.Task> GetDataset(string dataset, string subject, string fields) { }
+ }
+}
namespace Altinn.App.Core.Infrastructure.Clients.Events
{
public class EventsClient : Altinn.App.Core.Internal.Events.IEventsClient
@@ -2989,6 +3003,13 @@ namespace Altinn.App.Core.Internal.Auth
string GetUserToken();
}
}
+namespace Altinn.App.Core.Internal.Dan
+{
+ public interface IDanClient
+ {
+ System.Threading.Tasks.Task> GetDataset(string dataset, string subject, string fields);
+ }
+}
namespace Altinn.App.Core.Internal.Data
{
public static class FormDataWrapperFactory
diff --git a/test/Altinn.App.Tests.Common/Fixtures/MockedServiceCollection.cs b/test/Altinn.App.Tests.Common/Fixtures/MockedServiceCollection.cs
index 05c78079ac..b0d055e22b 100644
--- a/test/Altinn.App.Tests.Common/Fixtures/MockedServiceCollection.cs
+++ b/test/Altinn.App.Tests.Common/Fixtures/MockedServiceCollection.cs
@@ -6,10 +6,12 @@
using Altinn.App.Core.Configuration;
using Altinn.App.Core.Features;
using Altinn.App.Core.Helpers.Serialization;
+using Altinn.App.Core.Infrastructure.Clients.Dan;
using Altinn.App.Core.Infrastructure.Clients.Storage;
using Altinn.App.Core.Internal.App;
using Altinn.App.Core.Internal.AppModel;
using Altinn.App.Core.Internal.Auth;
+using Altinn.App.Core.Internal.Dan;
using Altinn.App.Core.Internal.Data;
using Altinn.App.Core.Internal.Instances;
using Altinn.App.Core.Internal.Texts;
@@ -98,6 +100,8 @@ public void TryAddCommonServices()
_services.AddHttpClient().ConfigurePrimaryHttpMessageHandler(() => Storage);
_services.AddHttpClient().ConfigurePrimaryHttpMessageHandler(() => Storage);
+ _services.TryAddSingleton();
+
_services.TryAddSingleton();
_services.AddLogging(builder =>
{