diff --git a/.github/workflows/build-and-analyze.yml b/.github/workflows/build-and-analyze.yml
index c39cb10..c13a5c8 100644
--- a/.github/workflows/build-and-analyze.yml
+++ b/.github/workflows/build-and-analyze.yml
@@ -14,6 +14,20 @@ jobs:
name: Build, test & analyze
if: ((github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false) || github.event_name == 'push') && github.repository_owner == 'Altinn'
runs-on: ubuntu-latest
+ services:
+ postgres:
+ image: postgres:16
+ env:
+ POSTGRES_USER: platform_profile_admin
+ POSTGRES_PASSWORD: Password
+ POSTGRES_DB: profiledb
+ options: >-
+ --health-cmd pg_isready
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 5
+ ports:
+ - 5432:5432
steps:
- name: Setup .NET
uses: actions/setup-dotnet@v4
@@ -28,6 +42,13 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
+ - name: Setup PostgreSQL
+ run: |
+ chmod +x dbsetup.sh
+ ./dbsetup.sh
+ - name: Restart database to enable config changes
+ run: |
+ docker restart $(docker ps -q)
- name: Cache SonarCloud packages
uses: actions/cache@v4
with:
diff --git a/dbsetup.sh b/dbsetup.sh
new file mode 100644
index 0000000..f48a834
--- /dev/null
+++ b/dbsetup.sh
@@ -0,0 +1,13 @@
+ #!/bin/bash
+export PGPASSWORD=Password
+
+# alter max connections
+psql -h localhost -p 5432 -U platform_profile_admin -d profiledb \
+-c "ALTER SYSTEM SET max_connections TO '200';"
+
+# set up platform_profile role
+psql -h localhost -p 5432 -U platform_profile_admin -d profiledb \
+-c "DO \$\$
+ BEGIN CREATE ROLE platform_profile WITH LOGIN PASSWORD 'Password';
+ EXCEPTION WHEN duplicate_object THEN RAISE NOTICE '%, skipping', SQLERRM USING ERRCODE = SQLSTATE;
+ END \$\$;"
diff --git a/src/Altinn.Profile.Core/Altinn.Profile.Core.csproj b/src/Altinn.Profile.Core/Altinn.Profile.Core.csproj
index abb629b..f778641 100644
--- a/src/Altinn.Profile.Core/Altinn.Profile.Core.csproj
+++ b/src/Altinn.Profile.Core/Altinn.Profile.Core.csproj
@@ -10,12 +10,9 @@
+
-
-
-
-
diff --git a/src/Altinn.Profile.Core/ContactRegister/ContactRegisterChangesLog.cs b/src/Altinn.Profile.Core/ContactRegister/ContactRegisterChangesLog.cs
new file mode 100644
index 0000000..b50ed4a
--- /dev/null
+++ b/src/Altinn.Profile.Core/ContactRegister/ContactRegisterChangesLog.cs
@@ -0,0 +1,36 @@
+using System.Collections.Immutable;
+using System.Text.Json.Serialization;
+
+using Altinn.Profile.Core.Person.ContactPreferences;
+
+namespace Altinn.Profile.Core.ContactRegister;
+
+///
+/// Represents the changes to a person's contact preferences from the contact register.
+///
+public record ContactRegisterChangesLog
+{
+ ///
+ /// Gets the collection of snapshots representing the changes to a person's contact preferences.
+ ///
+ [JsonPropertyName("list")]
+ public IImmutableList? ContactPreferencesSnapshots { get; init; }
+
+ ///
+ /// Gets the ending change identifier, which indicates the point at which the system should stop retrieving changes.
+ ///
+ [JsonPropertyName("tilEndringsId")]
+ public long? EndingIdentifier { get; init; }
+
+ ///
+ /// Gets the most recent change identifier, which represents the last change that was processed by the system.
+ ///
+ [JsonPropertyName("sisteEndringsId")]
+ public long? LatestChangeIdentifier { get; init; }
+
+ ///
+ /// Gets the starting change identifier indicating the point from which the system begins retrieving changes.
+ ///
+ [JsonPropertyName("fraEndringsId")]
+ public long? StartingIdentifier { get; init; }
+}
diff --git a/src/Altinn.Profile.Core/ContactRegister/IContactRegisterHttpClient.cs b/src/Altinn.Profile.Core/ContactRegister/IContactRegisterHttpClient.cs
new file mode 100644
index 0000000..9383d3a
--- /dev/null
+++ b/src/Altinn.Profile.Core/ContactRegister/IContactRegisterHttpClient.cs
@@ -0,0 +1,19 @@
+using Altinn.Profile.Core.ContactRegister;
+
+namespace Altinn.Profile.Integrations.ContactRegister;
+
+///
+/// An HTTP client to interact with the contact register.
+///
+public interface IContactRegisterHttpClient
+{
+ ///
+ /// Retrieves the changes in persons' contact details from the specified endpoint.
+ ///
+ /// The URL of the endpoint to retrieve contact details changes from.
+ /// The starting identifier for retrieving contact details changes.
+ ///
+ /// A task that represents the asynchronous operation.
+ ///
+ Task GetContactDetailsChangesAsync(string endpointUrl, long startingIdentifier);
+}
diff --git a/src/Altinn.Profile.Core/ContactRegister/IContactRegisterService.cs b/src/Altinn.Profile.Core/ContactRegister/IContactRegisterService.cs
new file mode 100644
index 0000000..fb299ef
--- /dev/null
+++ b/src/Altinn.Profile.Core/ContactRegister/IContactRegisterService.cs
@@ -0,0 +1,16 @@
+namespace Altinn.Profile.Core.ContactRegister;
+
+///
+/// Interface for handling changes in a person's contact preferences.
+///
+public interface IContactRegisterService
+{
+ ///
+ /// Asynchronously retrieves the changes in contact preferences for all persons starting from a given number.
+ ///
+ /// The identifier from which to start retrieving the data.
+ ///
+ /// A task that represents the asynchronous operation.
+ ///
+ Task RetrieveContactDetailsChangesAsync(long startingIdentifier);
+}
diff --git a/src/Altinn.Profile.Core/Domain/IRepository.cs b/src/Altinn.Profile.Core/Domain/IRepository.cs
deleted file mode 100644
index 6a76054..0000000
--- a/src/Altinn.Profile.Core/Domain/IRepository.cs
+++ /dev/null
@@ -1,93 +0,0 @@
-#nullable enable
-
-namespace Altinn.Profile.Core.Domain;
-
-///
-/// Defines generic methods for handling entities in a repository.
-///
-/// The type of the entity.
-public interface IRepository
- where T : class
-{
- ///
- /// Asynchronously retrieves all entities.
- ///
- /// A task that represents the asynchronous operation. The task result contains a collection of entities.
- Task> GetAllAsync();
-
- ///
- /// Asynchronously retrieves entities with optional filtering, sorting, and pagination.
- ///
- /// Optional filter criteria.
- /// Optional ordering criteria.
- /// The number of entities to skip.
- /// The number of entities to take.
- /// A task that represents the asynchronous operation. The task result contains a collection of filtered, sorted, and paginated entities.
- Task> GetAsync(
- Func? filter = null,
- Func, IOrderedEnumerable>? orderBy = null,
- int? skip = null,
- int? take = null);
-
- ///
- /// Asynchronously retrieves an entity by its identifier.
- ///
- /// The identifier of the entity.
- /// A task that represents the asynchronous operation. The task result contains the entity that matches the identifier.
- Task GetByIdAsync(string id);
-
- ///
- /// Asynchronously checks if an entity exists by its identifier.
- ///
- /// The identifier of the entity.
- /// A task that represents the asynchronous operation. The task result contains a boolean indicating the existence of the entity.
- Task ExistsAsync(string id);
-
- ///
- /// Asynchronously adds a new entity.
- ///
- /// The entity to add.
- /// A task that represents the asynchronous operation. The task result contains the added entity.
- Task AddAsync(T entity);
-
- ///
- /// Asynchronously adds multiple entities.
- ///
- /// The entities to add.
- /// A task that represents the asynchronous operation.
- Task AddRangeAsync(IEnumerable entities);
-
- ///
- /// Asynchronously updates an existing entity.
- ///
- /// The entity to update.
- /// A task that represents the asynchronous operation.
- Task UpdateAsync(T entity);
-
- ///
- /// Asynchronously updates multiple entities.
- ///
- /// The entities to update.
- /// A task that represents the asynchronous operation.
- Task UpdateRangeAsync(IEnumerable entities);
-
- ///
- /// Asynchronously deletes an entity by its identifier.
- ///
- /// The identifier of the entity to delete.
- /// A task that represents the asynchronous operation.
- Task DeleteAsync(string id);
-
- ///
- /// Asynchronously deletes multiple entities by their identifiers.
- ///
- /// The identifiers of the entities to delete.
- /// A task that represents the asynchronous operation.
- Task DeleteRangeAsync(IEnumerable ids);
-
- ///
- /// Saves changes to the data source asynchronously.
- ///
- /// A task that represents the asynchronous save operation. The task result contains the number of state entries written to the data source.
- Task SaveChangesAsync();
-}
diff --git a/src/Altinn.Profile.Core/Extensions/StringExtensions.cs b/src/Altinn.Profile.Core/Extensions/StringExtensions.cs
index b59627c..256ea17 100644
--- a/src/Altinn.Profile.Core/Extensions/StringExtensions.cs
+++ b/src/Altinn.Profile.Core/Extensions/StringExtensions.cs
@@ -9,6 +9,28 @@ namespace Altinn.Profile.Core.Extensions;
///
public static partial class StringExtensions
{
+ ///
+ /// Validates the given URL.
+ ///
+ /// The URL to validate.
+ /// True if the URL is valid; otherwise, false.
+ public static bool IsValidUrl(this string url)
+ {
+ if (string.IsNullOrWhiteSpace(url))
+ {
+ return false;
+ }
+
+ try
+ {
+ return Uri.TryCreate(url, UriKind.Absolute, out Uri? uriResult) && (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps);
+ }
+ catch (UriFormatException)
+ {
+ return false;
+ }
+ }
+
///
/// Determines whether a given string consists of only digits.
///
@@ -49,48 +71,48 @@ public static bool IsDigitsOnly(this string input)
}
///
- /// Determines whether a given string represents a valid format for a Norwegian Social Security Number (SSN).
+ /// Determines whether a given string represents a valid format for a Norwegian national identity mumber.
///
- /// The Norwegian Social Security Number (SSN) to validate.
+ /// The Norwegian national identity number to validate.
/// Indicates whether to validate the control digits.
///
- /// true if the given string represents a valid format for a Norwegian Social Security Number (SSN) and, if specified, the control digits are valid; otherwise, false.
+ /// true if the given string represents a valid format for a Norwegian national identity number and, if specified, the control digits are valid; otherwise, false.
///
///
- /// A valid Norwegian Social Security Number (SSN) is an 11-digit number where:
+ /// A valid Norwegian national identity number is an 11-digit number where:
/// - The first six digits represent the date of birth in the format DDMMYY.
/// - The next three digits are an individual number where the first digit indicates the century of birth.
/// - The last two digits are control digits.
///
- /// Thrown when the individual number part of the SSN cannot be parsed into an integer.
+ /// Thrown when the individual number part of the national identity number cannot be parsed into an integer.
/// Thrown when the parsed date is outside the range of DateTime.
- public static bool IsValidSocialSecurityNumber(this string socialSecurityNumber, bool controlDigits = true)
+ public static bool IsValidNationalIdentityNumber(this string nationalIdentityNumber, bool controlDigits = true)
{
- if (string.IsNullOrWhiteSpace(socialSecurityNumber) || socialSecurityNumber.Length != 11)
+ if (string.IsNullOrWhiteSpace(nationalIdentityNumber) || nationalIdentityNumber.Length != 11)
{
return false;
}
// Return the cached result if the given string has been checked once.
- if (CachedSocialSecurityNumber.TryGetValue(socialSecurityNumber, out var cachedResult))
+ if (CachedNationalIdentityNumber.TryGetValue(nationalIdentityNumber, out var cachedResult))
{
return cachedResult;
}
- ReadOnlySpan socialSecurityNumberSpan = socialSecurityNumber.AsSpan();
+ ReadOnlySpan nationalIdentityNumberSpan = nationalIdentityNumber.AsSpan();
- for (int i = 0; i < socialSecurityNumberSpan.Length; i++)
+ for (int i = 0; i < nationalIdentityNumberSpan.Length; i++)
{
- if (!char.IsDigit(socialSecurityNumberSpan[i]))
+ if (!char.IsDigit(nationalIdentityNumberSpan[i]))
{
return false;
}
}
- // Extract parts of the Social Security Number (SSN) using slicing.
- ReadOnlySpan datePart = socialSecurityNumberSpan[..6];
- ReadOnlySpan controlDigitsPart = socialSecurityNumberSpan[9..11];
- ReadOnlySpan individualNumberPart = socialSecurityNumberSpan[6..9];
+ // Extract parts of the national identity number using slicing.
+ ReadOnlySpan datePart = nationalIdentityNumberSpan[..6];
+ ReadOnlySpan individualNumberPart = nationalIdentityNumberSpan[6..9];
+ ReadOnlySpan controlDigitsPart = nationalIdentityNumberSpan[9..11];
// If parsing the individual number part fails, return false.
if (!int.TryParse(individualNumberPart, out _))
@@ -104,17 +126,17 @@ public static bool IsValidSocialSecurityNumber(this string socialSecurityNumber,
return false;
}
- var isValidSocialSecurityNumber = !controlDigits || CalculateControlDigits(socialSecurityNumberSpan[..9].ToString()) == controlDigitsPart.ToString();
+ var isValidNationalIdentityNumber = !controlDigits || CalculateControlDigits(nationalIdentityNumberSpan[..9].ToString()) == controlDigitsPart.ToString();
- CachedSocialSecurityNumber.TryAdd(socialSecurityNumber, isValidSocialSecurityNumber);
+ CachedNationalIdentityNumber.TryAdd(nationalIdentityNumber, isValidNationalIdentityNumber);
- return isValidSocialSecurityNumber;
+ return isValidNationalIdentityNumber;
}
///
- /// Calculates the control digits used to validate a Norwegian Social Security Number.
+ /// Calculates the control digits used to validate a Norwegian national identity number.
///
- /// The first nine digits of the Social Security Number.
+ /// The first nine digits of the national identity number.
/// A represents the two control digits.
private static string CalculateControlDigits(string firstNineDigits)
{
@@ -167,12 +189,12 @@ private static int CalculateControlDigit(string digits, int[] weights)
private static partial Regex DigitsOnlyRegex();
///
- /// A cache for storing the validation results of Norwegian Social Security Numbers (SSNs).
+ /// A cache for storing the validation results of Norwegian national identity numbers.
///
///
- /// This cache helps to avoid redundant validation checks for SSNs that have already been processed.
- /// It maps the SSN as a string to a boolean indicating whether the SSN is valid (true) or not (false).
- /// Utilizing this cache can significantly improve performance for applications that frequently validate the same SSNs.
+ /// This cache helps to avoid redundant validation checks for national identity numbers (nin) that have already been processed.
+ /// It maps the identity numbers as a string to a boolean indicating whether the number is valid (true) or not (false).
+ /// Utilizing this cache can significantly improve performance for applications that frequently validate the same numbers.
///
- private static ConcurrentDictionary CachedSocialSecurityNumber => new();
+ private static ConcurrentDictionary CachedNationalIdentityNumber => new();
}
diff --git a/src/Altinn.Profile.Core/Integrations/IUnitProfileRepository.cs b/src/Altinn.Profile.Core/Integrations/IUnitProfileRepository.cs
index 077a5f2..218d90c 100644
--- a/src/Altinn.Profile.Core/Integrations/IUnitProfileRepository.cs
+++ b/src/Altinn.Profile.Core/Integrations/IUnitProfileRepository.cs
@@ -1,17 +1,16 @@
using Altinn.Profile.Core.Unit.ContactPoints;
-namespace Altinn.Profile.Core.Integrations
+namespace Altinn.Profile.Core.Integrations;
+
+///
+/// Interface for accessing user profile services related to unit contact points.
+///
+public interface IUnitProfileRepository
{
///
- /// Interface describing a client for the user profile service
+ /// Retrieves a list of user-registered contact points based on the specified lookup criteria.
///
- public interface IUnitProfileRepository
- {
- ///
- /// Provides a list of user registered contact points based on the lookup criteria
- ///
- /// Lookup object containing a list of organizations and a resource
- /// A list of unit contact points
- Task> GetUserRegisteredContactPoints(UnitContactPointLookup lookup);
- }
+ /// An object containing a list of organization numbers and a resource ID to filter the contact points.
+ /// A task that represents the asynchronous operation. The task result contains a object with a on success, or a boolean indicating failure.
+ Task> GetUserRegisteredContactPoints(UnitContactPointLookup lookup);
}
diff --git a/src/Altinn.Profile.Core/Integrations/IUserProfileRepository.cs b/src/Altinn.Profile.Core/Integrations/IUserProfileRepository.cs
index ff673b7..9790058 100644
--- a/src/Altinn.Profile.Core/Integrations/IUserProfileRepository.cs
+++ b/src/Altinn.Profile.Core/Integrations/IUserProfileRepository.cs
@@ -3,42 +3,42 @@
namespace Altinn.Profile.Core.Integrations;
///
-/// Interface describing a client for the user profile service
+/// Interface for accessing user profile services.
///
public interface IUserProfileRepository
{
///
- /// Method that fetches a user based on a user id
+ /// Retrieves a user profile based on the user's social security number (SSN).
///
- /// The user id
- /// User profile with given user id or a boolean if failure.
- Task> GetUser(int userId);
+ /// The user's social security number.
+ /// A task that represents the asynchronous operation. The task result contains a object with a on success, or a boolean indicating failure.
+ Task> GetUser(string ssn);
///
- /// Method that fetches a user based on ssn.
+ /// Retrieves a user profile based on the user's ID.
///
- /// The user's ssn.
- /// User profile connected to given ssn or a boolean if failure.
- Task> GetUser(string ssn);
+ /// The user ID.
+ /// A task that represents the asynchronous operation. The task result contains a object with a on success, or a boolean indicating failure.
+ Task> GetUser(int userId);
///
- /// Method that fetches a user based on a user uuid
+ /// Retrieves a user profile based on the user's username.
///
- /// The user uuid
- /// User profile with given user uuid or a boolean if failure.
- Task> GetUserByUuid(Guid userUuid);
+ /// The user's username.
+ /// A task that represents the asynchronous operation. The task result contains a object with a on success, or a boolean indicating failure.
+ Task> GetUserByUsername(string username);
///
- /// Method that fetches a list of users based on a list of user uuid
+ /// Retrieves a user profile based on the user's UUID.
///
- /// The list of user uuids
- /// List of User profiles with given user uuids or a boolean if failure.
- Task, bool>> GetUserListByUuid(List userUuidList);
+ /// The user UUID.
+ /// A task that represents the asynchronous operation. The task result contains a object with a on success, or a boolean indicating failure.
+ Task> GetUserByUuid(Guid userUuid);
///
- /// Method that fetches a user based on username.
+ /// Retrieves a list of user profiles based on a list of user UUIDs.
///
- /// The user's username.
- /// User profile connected to given username or a boolean if failure.
- Task> GetUserByUsername(string username);
+ /// The list of user UUIDs.
+ /// A task that represents the asynchronous operation. The task result contains a object with a list of on success, or a boolean indicating failure.
+ Task, bool>> GetUserListByUuid(List userUuidList);
}
diff --git a/src/Altinn.Profile.Core/Person.ContactPreferences/PersonContactDetailsSnapshot.cs b/src/Altinn.Profile.Core/Person.ContactPreferences/PersonContactDetailsSnapshot.cs
new file mode 100644
index 0000000..525abbc
--- /dev/null
+++ b/src/Altinn.Profile.Core/Person.ContactPreferences/PersonContactDetailsSnapshot.cs
@@ -0,0 +1,57 @@
+using System.Text.Json.Serialization;
+
+namespace Altinn.Profile.Core.Person.ContactPreferences;
+
+///
+/// Represents a snapshot of the contact details retrieved from the changes log.
+///
+public record PersonContactDetailsSnapshot
+{
+ ///
+ /// Gets the email address of the person.
+ ///
+ [JsonPropertyName("epostadresse")]
+ public string? Email { get; init; }
+
+ ///
+ /// Gets the date and time when the email address was last verified.
+ ///
+ [JsonPropertyName("epostadresse_sist_verifisert")]
+ public DateTime? EmailLastVerified { get; init; }
+
+ ///
+ /// Gets the date and time when the email address was last updated.
+ ///
+ [JsonPropertyName("epostadresse_oppdatert")]
+ public DateTime? EmailLastUpdated { get; init; }
+
+ ///
+ /// Gets a value indicating whether the email address is duplicated.
+ ///
+ [JsonPropertyName("epostadresse_duplisert")]
+ public string? IsEmailDuplicated { get; init; }
+
+ ///
+ /// Gets a value indicating whether the mobile phone number is duplicated.
+ ///
+ [JsonPropertyName("mobiltelefonnummer_duplisert")]
+ public string? IsMobileNumberDuplicated { get; init; }
+
+ ///
+ /// Gets the mobile phone number of the person.
+ ///
+ [JsonPropertyName("mobiltelefonnummer")]
+ public string? MobileNumber { get; init; }
+
+ ///
+ /// Gets the date and time when the mobile phone number was last verified.
+ ///
+ [JsonPropertyName("mobiltelefonnummer_sist_verifisert")]
+ public DateTime? MobileNumberLastVerified { get; init; }
+
+ ///
+ /// Gets the date and time when the mobile phone number was last updated.
+ ///
+ [JsonPropertyName("mobiltelefonnummer_oppdatert")]
+ public DateTime? MobileNumberLastUpdated { get; init; }
+}
diff --git a/src/Altinn.Profile.Integrations/Entities/PersonContactDetails.cs b/src/Altinn.Profile.Core/Person.ContactPreferences/PersonContactPreferences.cs
similarity index 78%
rename from src/Altinn.Profile.Integrations/Entities/PersonContactDetails.cs
rename to src/Altinn.Profile.Core/Person.ContactPreferences/PersonContactPreferences.cs
index 9de6fd9..a853379 100644
--- a/src/Altinn.Profile.Integrations/Entities/PersonContactDetails.cs
+++ b/src/Altinn.Profile.Core/Person.ContactPreferences/PersonContactPreferences.cs
@@ -1,16 +1,16 @@
#nullable enable
-namespace Altinn.Profile.Integrations.Entities;
+namespace Altinn.Profile.Core.Person.ContactPreferences;
///
/// Represents a person's contact details.
///
-public record PersonContactDetails : IPersonContactDetails
+public record PersonContactPreferences
{
///
- /// Gets the national identity number of the person.
+ /// Gets the email address of the person.
///
- public required string NationalIdentityNumber { get; init; }
+ public string? Email { get; init; }
///
/// Gets a value indicating whether the person opts out of being contacted.
@@ -18,17 +18,17 @@ public record PersonContactDetails : IPersonContactDetails
public bool? IsReserved { get; init; }
///
- /// Gets the mobile phone number of the person.
+ /// Gets the language code of the person, represented as an ISO 639-1 code.
///
- public string? MobilePhoneNumber { get; init; }
+ public string? LanguageCode { get; init; }
///
- /// Gets the email address of the person.
+ /// Gets the mobile phone number of the person.
///
- public string? EmailAddress { get; init; }
+ public string? MobileNumber { get; init; }
///
- /// Gets the language code of the person, represented as an ISO 639-1 code.
+ /// Gets the national identity number of the person.
///
- public string? LanguageCode { get; init; }
+ public required string NationalIdentityNumber { get; init; }
}
diff --git a/src/Altinn.Profile.Core/Person.ContactPreferences/PersonContactPreferencesSnapshot.cs b/src/Altinn.Profile.Core/Person.ContactPreferences/PersonContactPreferencesSnapshot.cs
new file mode 100644
index 0000000..73fd281
--- /dev/null
+++ b/src/Altinn.Profile.Core/Person.ContactPreferences/PersonContactPreferencesSnapshot.cs
@@ -0,0 +1,51 @@
+using System.Text.Json.Serialization;
+
+namespace Altinn.Profile.Core.Person.ContactPreferences;
+
+///
+/// Represents a log of changes to a person's contact preferences, including contact information, language preference, notification status, and other details.
+///
+public class PersonContactPreferencesSnapshot
+{
+ ///
+ /// Gets the contact information details of the person.
+ ///
+ [JsonPropertyName("kontaktinformasjon")]
+ public PersonContactDetailsSnapshot? ContactDetailsSnapshot { get; init; }
+
+ ///
+ /// Gets the language preference of the person.
+ ///
+ [JsonPropertyName("spraak")]
+ public string? Language { get; init; }
+
+ ///
+ /// Gets the date and time when the person's language preference was last updated.
+ ///
+ [JsonPropertyName("spraak_oppdatert")]
+ public DateTime? LanguageLastUpdated { get; init; }
+
+ ///
+ /// Gets the notification status of the person.
+ ///
+ [JsonPropertyName("varslingsstatus")]
+ public string? NotificationStatus { get; init; }
+
+ ///
+ /// Gets the unique identifier of the person.
+ ///
+ [JsonPropertyName("personidentifikator")]
+ public string? PersonIdentifier { get; init; }
+
+ ///
+ /// Gets the reservation details of the person.
+ ///
+ [JsonPropertyName("reservasjon")]
+ public string? Reservation { get; init; }
+
+ ///
+ /// Gets the current status of the person.
+ ///
+ [JsonPropertyName("status")]
+ public string? Status { get; init; }
+}
diff --git a/src/Altinn.Profile.Integrations/Altinn.Profile.Integrations.csproj b/src/Altinn.Profile.Integrations/Altinn.Profile.Integrations.csproj
index 8e98664..436159a 100644
--- a/src/Altinn.Profile.Integrations/Altinn.Profile.Integrations.csproj
+++ b/src/Altinn.Profile.Integrations/Altinn.Profile.Integrations.csproj
@@ -9,7 +9,6 @@
-
diff --git a/src/Altinn.Profile.Integrations/ContactRegister/ContactAndReservationChangesException.cs b/src/Altinn.Profile.Integrations/ContactRegister/ContactAndReservationChangesException.cs
new file mode 100644
index 0000000..4531090
--- /dev/null
+++ b/src/Altinn.Profile.Integrations/ContactRegister/ContactAndReservationChangesException.cs
@@ -0,0 +1,36 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace Altinn.Profile.Integrations.ContactRegister;
+
+///
+/// Represents errors that occur during order processing operations.
+///
+[ExcludeFromCodeCoverage]
+public class ContactAndReservationChangesException : Exception
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ContactAndReservationChangesException() : base()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class
+ /// with a specified error message.
+ ///
+ /// The message that describes the error.
+ public ContactAndReservationChangesException(string message) : base(message)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class
+ /// with a specified error message and a reference to the inner exception that is the cause of this exception.
+ ///
+ /// The error message that explains the reason for the exception.
+ /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified.
+ public ContactAndReservationChangesException(string message, Exception inner) : base(message, inner)
+ {
+ }
+}
diff --git a/src/Altinn.Profile.Integrations/ContactRegister/ContactRegisterHttpClient.cs b/src/Altinn.Profile.Integrations/ContactRegister/ContactRegisterHttpClient.cs
new file mode 100644
index 0000000..f7910f5
--- /dev/null
+++ b/src/Altinn.Profile.Integrations/ContactRegister/ContactRegisterHttpClient.cs
@@ -0,0 +1,69 @@
+using System.Text;
+using System.Text.Json;
+
+using Altinn.Profile.Core.ContactRegister;
+using Altinn.Profile.Core.Extensions;
+
+namespace Altinn.Profile.Integrations.ContactRegister;
+
+///
+/// An HTTP client to interact with the contact register.
+///
+public class ContactRegisterHttpClient : IContactRegisterHttpClient
+{
+ private readonly HttpClient _httpClient;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The HTTP client to interact with the contact register.
+ public ContactRegisterHttpClient(HttpClient httpClient)
+ {
+ _httpClient = httpClient;
+ }
+
+ ///
+ /// Retrieves the changes in persons' contact details from the specified endpoint.
+ ///
+ /// The URL of the endpoint to retrieve contact details changes from.
+ /// The starting identifier for retrieving contact details changes.
+ ///
+ /// A task that represents the asynchronous operation with the returned values.
+ ///
+ /// The URL is invalid. - endpointUrl
+ public async Task GetContactDetailsChangesAsync(string endpointUrl, long startingIdentifier)
+ {
+ if (!endpointUrl.IsValidUrl())
+ {
+ throw new ArgumentException("The endpoint URL is invalid.", nameof(endpointUrl));
+ }
+
+ if (startingIdentifier < 0)
+ {
+ throw new ArgumentException("The starting position is invalid.", nameof(endpointUrl));
+ }
+
+ var request = new HttpRequestMessage(HttpMethod.Post, endpointUrl)
+ {
+ Content = new StringContent($"{{\"fraEndringsId\": {startingIdentifier}}}", Encoding.UTF8, "application/json")
+ };
+
+ var response = await _httpClient.SendAsync(request);
+
+ if (!response.IsSuccessStatusCode)
+ {
+ throw new ContactAndReservationChangesException("Failed to retrieve contact details changes.");
+ }
+
+ var responseData = await response.Content.ReadAsStringAsync();
+
+ var responseObject = JsonSerializer.Deserialize(responseData);
+
+ if (responseObject == null || responseObject.ContactPreferencesSnapshots == null)
+ {
+ throw new ContactAndReservationChangesException("Failed to deserialize the response from the contact and reservation registry.");
+ }
+
+ return responseObject;
+ }
+}
diff --git a/src/Altinn.Profile.Integrations/ContactRegister/ContactRegisterService.cs b/src/Altinn.Profile.Integrations/ContactRegister/ContactRegisterService.cs
new file mode 100644
index 0000000..61dc10c
--- /dev/null
+++ b/src/Altinn.Profile.Integrations/ContactRegister/ContactRegisterService.cs
@@ -0,0 +1,41 @@
+using Altinn.Profile.Core.ContactRegister;
+
+namespace Altinn.Profile.Integrations.ContactRegister;
+
+///
+/// Implementation of the change log service for handling changes in a person's contact preferences.
+///
+internal class ContactRegisterService : IContactRegisterService
+{
+ private readonly IContactRegisterHttpClient _contactRegisterHttpClient;
+ private readonly ContactRegisterSettings _contactRegisterSettings;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The settings used to configure the contact register.
+ /// The HTTP client used to retrieve contact details changes.
+ public ContactRegisterService(ContactRegisterSettings contactRegisterSettings, IContactRegisterHttpClient contactRegisterHttpClient)
+ {
+ _contactRegisterSettings = contactRegisterSettings;
+ _contactRegisterHttpClient = contactRegisterHttpClient;
+ }
+
+ ///
+ /// Asynchronously retrieves the changes in contact preferences for all persons starting from a given number.
+ ///
+ /// The identifier from which to start retrieving the data.
+ ///
+ /// A task that represents the asynchronous operation.
+ ///
+ /// Thrown if the is null or empty.
+ public async Task RetrieveContactDetailsChangesAsync(long startingIdentifier)
+ {
+ if (string.IsNullOrWhiteSpace(_contactRegisterSettings.ChangesLogEndpoint))
+ {
+ throw new InvalidOperationException("The endpoint URL must not be null or empty.");
+ }
+
+ return await _contactRegisterHttpClient.GetContactDetailsChangesAsync(_contactRegisterSettings.ChangesLogEndpoint, startingIdentifier);
+ }
+}
diff --git a/src/Altinn.Profile.Integrations/ContactRegister/ContactRegisterSettings.cs b/src/Altinn.Profile.Integrations/ContactRegister/ContactRegisterSettings.cs
new file mode 100644
index 0000000..b3472b2
--- /dev/null
+++ b/src/Altinn.Profile.Integrations/ContactRegister/ContactRegisterSettings.cs
@@ -0,0 +1,21 @@
+#nullable enable
+
+using Altinn.ApiClients.Maskinporten.Config;
+
+namespace Altinn.Profile.Integrations.ContactRegister;
+
+///
+/// Represents the settings for managing contact details and reservation information for individuals.
+///
+public class ContactRegisterSettings
+{
+ ///
+ /// Gets the endpoint URL used to retrieve updates in the contact information for one or more individuals.
+ ///
+ public string? ChangesLogEndpoint { get; init; }
+
+ ///
+ /// Gets the settings required for Maskinporten authentication.
+ ///
+ public MaskinportenSettings? MaskinportenSettings { get; init; }
+}
diff --git a/src/Altinn.Profile.Integrations/Entities/IPersonContactDetails.cs b/src/Altinn.Profile.Integrations/Entities/IPersonContactDetails.cs
deleted file mode 100644
index cb1e7db..0000000
--- a/src/Altinn.Profile.Integrations/Entities/IPersonContactDetails.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-#nullable enable
-
-namespace Altinn.Profile.Integrations.Entities;
-
-///
-/// Represents a person's contact details.
-///
-public interface IPersonContactDetails
-{
- ///
- /// Gets the national identity number of the person.
- ///
- string NationalIdentityNumber { get; }
-
- ///
- /// Gets a value indicating whether the person opts out of being contacted.
- ///
- bool? IsReserved { get; }
-
- ///
- /// Gets the mobile phone number of the person.
- ///
- string? MobilePhoneNumber { get; }
-
- ///
- /// Gets the email address of the person.
- ///
- string? EmailAddress { get; }
-
- ///
- /// Gets the language code of the person, represented as an ISO 639-1 code.
- ///
- string? LanguageCode { get; }
-}
diff --git a/src/Altinn.Profile.Integrations/Entities/IPersonContactDetailsLookupResult.cs b/src/Altinn.Profile.Integrations/Entities/IPersonContactDetailsLookupResult.cs
deleted file mode 100644
index b717305..0000000
--- a/src/Altinn.Profile.Integrations/Entities/IPersonContactDetailsLookupResult.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-#nullable enable
-
-using System.Collections.Immutable;
-
-namespace Altinn.Profile.Integrations.Entities;
-
-///
-/// Represents the result of a lookup operation for contact details.
-///
-public interface IPersonContactDetailsLookupResult
-{
- ///
- /// Gets a list of national identity numbers that could not be matched with any person contact details.
- ///
- ///
- /// An of containing the unmatched national identity numbers.
- ///
- ImmutableList? UnmatchedNationalIdentityNumbers { get; }
-
- ///
- /// Gets a list of person contact details that were successfully matched during the lookup.
- ///
- ///
- /// An of containing the matched person contact details.
- ///
- ImmutableList? MatchedPersonContactDetails { get; }
-}
diff --git a/src/Altinn.Profile.Integrations/Entities/IPersonContactPreferencesLookupResult.cs b/src/Altinn.Profile.Integrations/Entities/IPersonContactPreferencesLookupResult.cs
new file mode 100644
index 0000000..9883f2d
--- /dev/null
+++ b/src/Altinn.Profile.Integrations/Entities/IPersonContactPreferencesLookupResult.cs
@@ -0,0 +1,28 @@
+#nullable enable
+using System.Collections.Immutable;
+
+using Altinn.Profile.Core.Person.ContactPreferences;
+
+namespace Altinn.Profile.Integrations.Entities;
+
+///
+/// Represents the result of a lookup operation for contact preferences.
+///
+public interface IPersonContactPreferencesLookupResult
+{
+ ///
+ /// Gets a list of person contact preferences that were successfully matched during the lookup.
+ ///
+ ///
+ /// An of containing the matched person contact preferences.
+ ///
+ ImmutableList? MatchedPersonContactPreferences { get; }
+
+ ///
+ /// Gets a list of national identity numbers that could not be matched with any person contact preferences.
+ ///
+ ///
+ /// An of containing the unmatched national identity numbers.
+ ///
+ ImmutableList? UnmatchedNationalIdentityNumbers { get; }
+}
diff --git a/src/Altinn.Profile.Integrations/Entities/PersonContactDetailsLookupResult.cs b/src/Altinn.Profile.Integrations/Entities/PersonContactDetailsLookupResult.cs
deleted file mode 100644
index 470e526..0000000
--- a/src/Altinn.Profile.Integrations/Entities/PersonContactDetailsLookupResult.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-#nullable enable
-
-using System.Collections.Immutable;
-
-namespace Altinn.Profile.Integrations.Entities;
-
-///
-/// Represents the result of a lookup operation for contact details.
-///
-public record PersonContactDetailsLookupResult : IPersonContactDetailsLookupResult
-{
- ///
- /// Gets a list of national identity numbers that could not be matched with any person contact details.
- ///
- ///
- /// An of containing the unmatched national identity numbers.
- ///
- public ImmutableList? UnmatchedNationalIdentityNumbers { get; init; }
-
- ///
- /// Gets a list of person contact details that were successfully matched during the lookup.
- ///
- ///
- /// An of containing the matched person contact details.
- ///
- public ImmutableList? MatchedPersonContactDetails { get; init; }
-}
diff --git a/src/Altinn.Profile.Integrations/Entities/PersonContactPreferencesLookupResult.cs b/src/Altinn.Profile.Integrations/Entities/PersonContactPreferencesLookupResult.cs
new file mode 100644
index 0000000..749ce01
--- /dev/null
+++ b/src/Altinn.Profile.Integrations/Entities/PersonContactPreferencesLookupResult.cs
@@ -0,0 +1,29 @@
+#nullable enable
+
+using System.Collections.Immutable;
+
+using Altinn.Profile.Core.Person.ContactPreferences;
+
+namespace Altinn.Profile.Integrations.Entities;
+
+///
+/// Represents the result of a lookup operation for contact preferences.
+///
+public record PersonContactPreferencesLookupResult : IPersonContactPreferencesLookupResult
+{
+ ///
+ /// Gets a list of person contact preferences that were successfully matched during the lookup.
+ ///
+ ///
+ /// An of containing the matched person contact preferences.
+ ///
+ public ImmutableList? MatchedPersonContactPreferences { get; init; }
+
+ ///
+ /// Gets a list of national identity numbers that could not be matched with any person contact preferences.
+ ///
+ ///
+ /// An of containing the unmatched national identity numbers.
+ ///
+ public ImmutableList? UnmatchedNationalIdentityNumbers { get; init; }
+}
diff --git a/src/Altinn.Profile.Integrations/Extensions/WebApplicationExtensions.cs b/src/Altinn.Profile.Integrations/Extensions/WebApplicationExtensions.cs
index 0831560..5f41f99 100644
--- a/src/Altinn.Profile.Integrations/Extensions/WebApplicationExtensions.cs
+++ b/src/Altinn.Profile.Integrations/Extensions/WebApplicationExtensions.cs
@@ -20,12 +20,10 @@ public static class WebApplicationExtensions
/// Configure and set up db
///
/// app
- /// is environment dev
/// the configuration collection
- public static void SetUpPostgreSql(this IApplicationBuilder app, bool isDevelopment, IConfiguration config)
+ public static void SetUpPostgreSql(this IApplicationBuilder app, IConfiguration config)
{
- PostgreSqlSettings? settings = config.GetSection("PostgreSQLSettings")
- .Get()
+ PostgreSqlSettings? settings = config.GetSection("PostgreSQLSettings").Get()
?? throw new ArgumentNullException(nameof(config), "Required PostgreSQLSettings is missing from application configuration");
if (settings.EnableDBConnection)
@@ -34,9 +32,7 @@ public static void SetUpPostgreSql(this IApplicationBuilder app, bool isDevelopm
string connectionString = string.Format(settings.AdminConnectionString, settings.ProfileDbAdminPwd);
- string fullWorkspacePath = isDevelopment ?
- Path.Combine(Directory.GetParent(Environment.CurrentDirectory)!.FullName, settings.MigrationScriptPath) :
- Path.Combine(Environment.CurrentDirectory, settings.MigrationScriptPath);
+ string fullWorkspacePath = Path.Combine(Environment.CurrentDirectory, settings.MigrationScriptPath);
app.UseYuniql(
new PostgreSqlDataService(traceService),
diff --git a/src/Altinn.Profile.Integrations/Mappings/PersonContactDetailsProfile.cs b/src/Altinn.Profile.Integrations/Mappings/PersonContactDetailsProfile.cs
deleted file mode 100644
index 64d7881..0000000
--- a/src/Altinn.Profile.Integrations/Mappings/PersonContactDetailsProfile.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-using Altinn.Profile.Integrations.Entities;
-
-namespace Altinn.Profile.Integrations.Mappings;
-
-///
-/// AutoMapper profile for mapping between and .
-///
-///
-/// This profile defines the mapping rules to convert a object into a instance.
-///
-public class PersonContactDetailsProfile : AutoMapper.Profile
-{
- ///
- /// Initializes a new instance of the class and configures the mappings.
- ///
- public PersonContactDetailsProfile()
- {
- CreateMap()
- .ForMember(dest => dest.IsReserved, opt => opt.MapFrom(src => src.Reservation))
- .ForMember(dest => dest.EmailAddress, opt => opt.MapFrom(src => src.EmailAddress))
- .ForMember(dest => dest.LanguageCode, opt => opt.MapFrom(src => src.LanguageCode))
- .ForMember(dest => dest.NationalIdentityNumber, opt => opt.MapFrom(src => src.FnumberAk))
- .ForMember(dest => dest.MobilePhoneNumber, opt => opt.MapFrom(src => src.MobilePhoneNumber));
- }
-}
diff --git a/src/Altinn.Profile.Integrations/Mappings/PersonContactPreferencesProfile.cs b/src/Altinn.Profile.Integrations/Mappings/PersonContactPreferencesProfile.cs
new file mode 100644
index 0000000..57a667f
--- /dev/null
+++ b/src/Altinn.Profile.Integrations/Mappings/PersonContactPreferencesProfile.cs
@@ -0,0 +1,26 @@
+using Altinn.Profile.Core.Person.ContactPreferences;
+using Altinn.Profile.Integrations.Entities;
+
+namespace Altinn.Profile.Integrations.Mappings;
+
+///
+/// AutoMapper profile for mapping between and .
+///
+///
+/// This profile defines the mapping rules to convert a object into a instance.
+///
+public class PersonContactPreferencesProfile : AutoMapper.Profile
+{
+ ///
+ /// Initializes a new instance of the class and configures the mappings.
+ ///
+ public PersonContactPreferencesProfile()
+ {
+ CreateMap()
+ .ForMember(dest => dest.Email, opt => opt.MapFrom(src => src.EmailAddress))
+ .ForMember(dest => dest.IsReserved, opt => opt.MapFrom(src => src.Reservation))
+ .ForMember(dest => dest.LanguageCode, opt => opt.MapFrom(src => src.LanguageCode))
+ .ForMember(dest => dest.MobileNumber, opt => opt.MapFrom(src => src.MobilePhoneNumber))
+ .ForMember(dest => dest.NationalIdentityNumber, opt => opt.MapFrom(src => src.FnumberAk));
+ }
+}
diff --git a/src/Altinn.Profile.Integrations/Mappings/PersonMappingProfile.cs b/src/Altinn.Profile.Integrations/Mappings/PersonMappingProfile.cs
new file mode 100644
index 0000000..fd35ae8
--- /dev/null
+++ b/src/Altinn.Profile.Integrations/Mappings/PersonMappingProfile.cs
@@ -0,0 +1,50 @@
+using Altinn.Profile.Core.Person.ContactPreferences;
+using Altinn.Profile.Integrations.Entities;
+
+namespace Altinn.Profile.Integrations.Mappings;
+
+///
+/// AutoMapper profile for mapping between and .
+///
+///
+/// This profile defines the mapping rules to convert a object into a instance.
+///
+public class PersonMappingProfile : AutoMapper.Profile
+{
+ ///
+ /// Initializes a new instance of the class and configures the mappings.
+ ///
+ public PersonMappingProfile()
+ {
+ CreateMap()
+ .ForMember(dest => dest.LanguageCode, opt => opt.MapFrom(src => src.Language))
+ .ForMember(dest => dest.FnumberAk, opt => opt.MapFrom(src => src.PersonIdentifier))
+ .ForMember(dest => dest.Reservation, opt => opt.MapFrom(src => src.Reservation == "JA"))
+ .ForMember(dest => dest.EmailAddress, opt => opt.MapFrom(src => GetContactDetail(src, detail => detail.Email)))
+ .ForMember(dest => dest.MobilePhoneNumber, opt => opt.MapFrom(src => GetContactDetail(src, detail => detail.MobileNumber)))
+ .ForMember(dest => dest.EmailAddressLastUpdated, opt => opt.MapFrom(src => GetContactDetailDate(src, detail => detail.EmailLastUpdated)))
+ .ForMember(dest => dest.EmailAddressLastVerified, opt => opt.MapFrom(src => GetContactDetailDate(src, detail => detail.EmailLastVerified)))
+ .ForMember(dest => dest.MobilePhoneNumberLastUpdated, opt => opt.MapFrom(src => GetContactDetailDate(src, detail => detail.MobileNumberLastUpdated)))
+ .ForMember(dest => dest.MobilePhoneNumberLastVerified, opt => opt.MapFrom(src => GetContactDetailDate(src, detail => detail.MobileNumberLastVerified)));
+ }
+
+ private static string? GetContactDetail(PersonContactPreferencesSnapshot src, Func selector)
+ {
+ return src.ContactDetailsSnapshot != null ? selector(src.ContactDetailsSnapshot) : null;
+ }
+
+ private static DateTime? GetContactDetailDate(PersonContactPreferencesSnapshot src, Func selector)
+ {
+ if (src.ContactDetailsSnapshot != null)
+ {
+ var date = selector(src.ContactDetailsSnapshot);
+
+ if (date.HasValue)
+ {
+ return date.Value.ToUniversalTime();
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/src/Altinn.Profile.Integrations/Migration/v0.02/01-setup-grants.sql b/src/Altinn.Profile.Integrations/Migration/v0.02/01-setup-grants.sql
new file mode 100644
index 0000000..2ed3f53
--- /dev/null
+++ b/src/Altinn.Profile.Integrations/Migration/v0.02/01-setup-grants.sql
@@ -0,0 +1,8 @@
+-- Grant access to the mailbox_supplier table
+GRANT SELECT,INSERT,UPDATE,DELETE ON TABLE contact_and_reservation.mailbox_supplier TO platform_profile;
+
+-- Grant access to the metadata table
+GRANT SELECT,INSERT,UPDATE,DELETE ON TABLE contact_and_reservation.metadata TO platform_profile;
+
+-- Grant access to the person table
+GRANT SELECT,INSERT,UPDATE,DELETE ON TABLE contact_and_reservation.person TO platform_profile;
diff --git a/src/Altinn.Profile.Integrations/Repositories/IMetadataRepository.cs b/src/Altinn.Profile.Integrations/Repositories/IMetadataRepository.cs
new file mode 100644
index 0000000..e6b67d2
--- /dev/null
+++ b/src/Altinn.Profile.Integrations/Repositories/IMetadataRepository.cs
@@ -0,0 +1,26 @@
+using Altinn.Profile.Core;
+
+namespace Altinn.Profile.Integrations.Repositories;
+
+///
+/// Defines a repository for handling metadata operations.
+///
+public interface IMetadataRepository
+{
+ ///
+ /// Asynchronously retrieves the latest change number from the metadata repository.
+ ///
+ ///
+ /// A task that represents the asynchronous operation. The task result contains a object with a value on success, or a value indicating failure.
+ ///
+ Task> GetLatestChangeNumberAsync();
+
+ ///
+ /// Asynchronously updates the latest change number from the metadata repository.
+ ///
+ /// The new changed number.
+ ///
+ /// A task that represents the asynchronous operation. The task result contains a object with a value on success, or a value indicating failure.
+ ///
+ Task> UpdateLatestChangeNumberAsync(long newNumber);
+}
diff --git a/src/Altinn.Profile.Integrations/Repositories/IPersonRepository.cs b/src/Altinn.Profile.Integrations/Repositories/IPersonRepository.cs
index 5a2a0f4..e920943 100644
--- a/src/Altinn.Profile.Integrations/Repositories/IPersonRepository.cs
+++ b/src/Altinn.Profile.Integrations/Repositories/IPersonRepository.cs
@@ -1,8 +1,7 @@
-#nullable enable
+using System.Collections.Immutable;
-using System.Collections.Immutable;
-
-using Altinn.Profile.Core.Domain;
+using Altinn.Profile.Core;
+using Altinn.Profile.Core.ContactRegister;
using Altinn.Profile.Integrations.Entities;
namespace Altinn.Profile.Integrations.Repositories;
@@ -10,14 +9,23 @@ namespace Altinn.Profile.Integrations.Repositories;
///
/// Defines a repository for handling person data operations.
///
-public interface IPersonRepository : IRepository
+public interface IPersonRepository
{
///
/// Asynchronously retrieves the contact details for multiple persons by their national identity numbers.
///
- /// A collection of national identity numbers to look up for.
+ /// A collection of national identity numbers to look up.
+ ///
+ /// A task that represents the asynchronous operation. The task result contains a object with an of objects representing the contact details of the persons on success, or a indicating failure.
+ ///
+ Task, bool>> GetContactDetailsAsync(IEnumerable nationalIdentityNumbers);
+
+ ///
+ /// Asynchronously synchronizes the changes in person contact preferences.
+ ///
+ /// The snapshots of person contact preferences to be synchronized.
///
- /// A task that represents the asynchronous operation. The task result contains an of objects representing the contact details of the persons.
+ /// A task that represents the asynchronous operation. The task result contains a object with a indicating success or failure.
///
- Task> GetContactDetailsAsync(IEnumerable nationalIdentityNumbers);
+ Task SyncPersonContactPreferencesAsync(ContactRegisterChangesLog personContactPreferencesSnapshots);
}
diff --git a/src/Altinn.Profile.Integrations/Repositories/MetadataRepository.cs b/src/Altinn.Profile.Integrations/Repositories/MetadataRepository.cs
new file mode 100644
index 0000000..22bcfe7
--- /dev/null
+++ b/src/Altinn.Profile.Integrations/Repositories/MetadataRepository.cs
@@ -0,0 +1,67 @@
+using Altinn.Profile.Core;
+using Altinn.Profile.Integrations.Entities;
+using Altinn.Profile.Integrations.Persistence;
+
+using Microsoft.EntityFrameworkCore;
+
+namespace Altinn.Profile.Integrations.Repositories;
+
+///
+/// Provides methods for handling metadata operations in the profile database.
+///
+///
+/// Initializes a new instance of the class.
+///
+/// The factory for creating database context instances.
+public class MetadataRepository(IDbContextFactory contextFactory) : IMetadataRepository
+{
+ private readonly IDbContextFactory _contextFactory = contextFactory;
+
+ ///
+ /// Asynchronously retrieves the latest change number from the metadata repository.
+ ///
+ ///
+ /// A task that represents the asynchronous operation. The task result contains a object with a value on success, or a value indicating failure.
+ ///
+ public async Task> GetLatestChangeNumberAsync()
+ {
+ using ProfileDbContext databaseContext = _contextFactory.CreateDbContext();
+
+ Metadata? metadataSingleRow = await databaseContext.Metadata.FirstOrDefaultAsync();
+
+ return metadataSingleRow != null ? metadataSingleRow.LatestChangeNumber : 0;
+ }
+
+ ///
+ /// Asynchronously updates the latest change number from the metadata repository.
+ ///
+ /// The new changed number.
+ ///
+ /// A task that represents the asynchronous operation. The task result contains a object with a value on success, or a value indicating failure.
+ ///
+ public async Task> UpdateLatestChangeNumberAsync(long newNumber)
+ {
+ if (newNumber < 0)
+ {
+ throw new ArgumentException("The new change number must be non-negative.", nameof(newNumber));
+ }
+
+ using ProfileDbContext databaseContext = _contextFactory.CreateDbContext();
+ Metadata? existingMetadata = await databaseContext.Metadata.FirstOrDefaultAsync();
+
+ if (existingMetadata != null)
+ {
+ databaseContext.Metadata.Remove(existingMetadata);
+ }
+
+ Metadata metadata = new()
+ {
+ Exported = DateTime.UtcNow,
+ LatestChangeNumber = newNumber
+ };
+
+ await databaseContext.Metadata.AddAsync(metadata);
+
+ return await databaseContext.SaveChangesAsync() > 0 ? newNumber : -1;
+ }
+}
diff --git a/src/Altinn.Profile.Integrations/Repositories/PersonRepository.cs b/src/Altinn.Profile.Integrations/Repositories/PersonRepository.cs
index fe9041e..6a4be4e 100644
--- a/src/Altinn.Profile.Integrations/Repositories/PersonRepository.cs
+++ b/src/Altinn.Profile.Integrations/Repositories/PersonRepository.cs
@@ -2,9 +2,14 @@
using System.Collections.Immutable;
+using Altinn.Profile.Core;
+using Altinn.Profile.Core.ContactRegister;
+using Altinn.Profile.Core.Person.ContactPreferences;
using Altinn.Profile.Integrations.Entities;
using Altinn.Profile.Integrations.Persistence;
+using AutoMapper;
+
using Microsoft.EntityFrameworkCore;
namespace Altinn.Profile.Integrations.Repositories;
@@ -13,39 +18,102 @@ namespace Altinn.Profile.Integrations.Repositories;
/// Defines a repository for handling person data operations.
///
///
-internal class PersonRepository : ProfileRepository, IPersonRepository
+///
+/// Initializes a new instance of the class.
+///
+/// The mapper instance used for object-object mapping.
+/// The factory for creating database context instances.
+///
+/// Thrown when the , or is null.
+///
+internal class PersonRepository(IMapper mapper, IDbContextFactory contextFactory) : IPersonRepository
{
- private readonly ProfileDbContext _context;
+ private readonly IMapper _mapper = mapper;
+ private readonly IDbContextFactory _contextFactory = contextFactory;
///
- /// Initializes a new instance of the class.
+ /// Asynchronously retrieves the contact details for multiple persons by their national identity numbers.
///
- /// The context.
- /// Thrown when the object is null.
- public PersonRepository(ProfileDbContext context)
+ /// A collection of national identity numbers to look up.
+ ///
+ /// A task that represents the asynchronous operation. The task result contains a object with an of objects representing the contact details of the persons on success, or a indicating failure.
+ ///
+ /// Thrown when the is null.
+ public async Task, bool>> GetContactDetailsAsync(IEnumerable nationalIdentityNumbers)
{
- _context = context ?? throw new ArgumentNullException(nameof(context));
+ ArgumentNullException.ThrowIfNull(nationalIdentityNumbers);
+
+ if (!nationalIdentityNumbers.Any())
+ {
+ return ImmutableList.Empty;
+ }
+
+ using ProfileDbContext databaseContext = await _contextFactory.CreateDbContextAsync();
+
+ List people = await databaseContext.People.Where(e => nationalIdentityNumbers.Contains(e.FnumberAk)).ToListAsync();
+
+ return people.ToImmutableList();
}
///
- /// Asynchronously retrieves the contact details for multiple persons by their national identity numbers.
+ /// Asynchronously synchronizes the changes in person contact preferences.
///
- /// A collection of national identity numbers to look up for.
+ /// The snapshots of person contact preferences to be synchronized.
///
- /// A task that represents the asynchronous operation. The task result contains an of objects representing the contact details of the persons.
+ /// A task that represents the asynchronous operation. The task result contains a object with a indicating success or failure.
///
- /// Thrown when the is null.
- public async Task> GetContactDetailsAsync(IEnumerable nationalIdentityNumbers)
+ public async Task SyncPersonContactPreferencesAsync(ContactRegisterChangesLog personContactPreferencesSnapshots)
{
- ArgumentNullException.ThrowIfNull(nationalIdentityNumbers);
+ ArgumentNullException.ThrowIfNull(personContactPreferencesSnapshots);
+ ArgumentNullException.ThrowIfNull(personContactPreferencesSnapshots.ContactPreferencesSnapshots);
- if (!nationalIdentityNumbers.Any())
+ ImmutableList distinctContactPreferences =
+ GetDistinctContactPreferences(personContactPreferencesSnapshots.ContactPreferencesSnapshots);
+
+ using ProfileDbContext databaseContext = await _contextFactory.CreateDbContextAsync();
+
+ foreach (PersonContactPreferencesSnapshot contactPreferenceSnapshot in distinctContactPreferences)
{
- return [];
+ Person person = _mapper.Map(contactPreferenceSnapshot);
+
+ Person? existingPerson = await databaseContext.People.FirstOrDefaultAsync(e => e.FnumberAk.Trim() == person.FnumberAk.Trim());
+
+ if (existingPerson is null)
+ {
+ await databaseContext.People.AddAsync(person);
+ }
+ else
+ {
+ existingPerson.Reservation = person.Reservation;
+ existingPerson.EmailAddress = person.EmailAddress;
+ existingPerson.EmailAddressLastUpdated = person.EmailAddressLastUpdated;
+ existingPerson.EmailAddressLastVerified = person.EmailAddressLastVerified;
+ existingPerson.MobilePhoneNumber = person.MobilePhoneNumber;
+ existingPerson.MobilePhoneNumberLastUpdated = person.MobilePhoneNumberLastUpdated;
+ existingPerson.MobilePhoneNumberLastVerified = person.MobilePhoneNumberLastVerified;
+ existingPerson.LanguageCode = person.LanguageCode;
+
+ databaseContext.People.Update(existingPerson);
+ }
}
- var people = await _context.People.Where(e => nationalIdentityNumbers.Contains(e.FnumberAk)).ToListAsync();
+ return await databaseContext.SaveChangesAsync();
+ }
- return [.. people];
+ ///
+ /// Finds distinct contact preferences by selecting the item with the largest values for the specified properties.
+ ///
+ /// The collection of contact preferences snapshots.
+ /// A list of distinct contact preferences.
+ private static ImmutableList GetDistinctContactPreferences(IEnumerable contactPreferencesSnapshots)
+ {
+ return contactPreferencesSnapshots.GroupBy(p => p.PersonIdentifier)
+ .Select(g => g.OrderByDescending(p => p.ContactDetailsSnapshot?.EmailLastUpdated)
+ .ThenByDescending(p => p.ContactDetailsSnapshot?.MobileNumberLastUpdated)
+ .ThenByDescending(p => p.ContactDetailsSnapshot?.EmailLastVerified)
+ .ThenByDescending(p => p.ContactDetailsSnapshot?.MobileNumberLastVerified)
+ .ThenByDescending(p => p.LanguageLastUpdated)
+ .First())
+ .ToImmutableList();
}
}
diff --git a/src/Altinn.Profile.Integrations/Repositories/ProfileRepository.cs b/src/Altinn.Profile.Integrations/Repositories/ProfileRepository.cs
deleted file mode 100644
index dee31f3..0000000
--- a/src/Altinn.Profile.Integrations/Repositories/ProfileRepository.cs
+++ /dev/null
@@ -1,134 +0,0 @@
-#nullable enable
-
-using Altinn.Profile.Core.Domain;
-
-namespace Altinn.Profile.Integrations.Repositories;
-
-///
-/// Generic repository for handling data access operations.
-///
-/// The type of the entity.
-///
-internal class ProfileRepository : IRepository
- where T : class
-{
- ///
- /// Initializes a new instance of the class.
- ///
- internal ProfileRepository()
- {
- }
-
- ///
- /// Asynchronously adds a new entity to the database.
- ///
- /// The entity to add.
- ///
- /// A task that represents the asynchronous operation. The task result contains the added entity.
- ///
- public Task AddAsync(T entity)
- {
- throw new NotImplementedException();
- }
-
- ///
- /// Adds multiple entities to the database asynchronously.
- ///
- /// The entities to add.
- /// A task that represents the asynchronous operation.
- public Task AddRangeAsync(IEnumerable entities)
- {
- throw new NotImplementedException();
- }
-
- ///
- /// Deletes an entity from the database asynchronously based on its identifier.
- ///
- /// The identifier of the entity to delete.
- /// A task that represents the asynchronous operation.
- public Task DeleteAsync(string id)
- {
- throw new NotImplementedException();
- }
-
- ///
- /// Deletes multiple entities from the database asynchronously based on their identifiers.
- ///
- /// The identifiers of the entities to delete.
- /// A task that represents the asynchronous operation.
- public Task DeleteRangeAsync(IEnumerable ids)
- {
- throw new NotImplementedException();
- }
-
- ///
- /// Checks whether an entity exists asynchronously based on its identifier.
- ///
- /// The identifier of the entity.
- /// A task that represents the asynchronous operation. The task result contains true if the entity exists, otherwise false.
- public Task ExistsAsync(string id)
- {
- throw new NotImplementedException();
- }
-
- ///
- /// Retrieves all entities from the database asynchronously.
- ///
- /// A task that represents the asynchronous operation. The task result contains a collection of all entities.
- public Task> GetAllAsync()
- {
- throw new NotImplementedException();
- }
-
- ///
- /// Retrieves entities based on a filter, with optional sorting, pagination, and filtering.
- ///
- /// A function to filter the entities.
- /// A function to order the entities.
- /// Number of entities to skip for pagination.
- /// Number of entities to take for pagination.
- /// A task that represents the asynchronous operation. The task result contains a collection of entities matching the criteria.
- public Task> GetAsync(Func? filter = null, Func, IOrderedEnumerable>? orderBy = null, int? skip = null, int? take = null)
- {
- throw new NotImplementedException();
- }
-
- ///
- /// Retrieves an entity asynchronously based on its identifier.
- ///
- /// The identifier of the entity to retrieve.
- /// A task that represents the asynchronous operation. The task result contains the entity if found, otherwise null.
- public Task GetByIdAsync(string id)
- {
- throw new NotImplementedException();
- }
-
- ///
- /// Saves the changes made in the context asynchronously.
- ///
- /// A task that represents the asynchronous operation. The task result contains the number of state entries written to the database.
- public Task SaveChangesAsync()
- {
- throw new NotImplementedException();
- }
-
- ///
- /// Updates an entity in the database asynchronously.
- ///
- /// The entity to update.
- /// A task that represents the asynchronous operation.
- public Task UpdateAsync(T entity)
- {
- throw new NotImplementedException();
- }
-
- ///
- /// Updates multiple entities in the database asynchronously.
- ///
- /// The entities to update.
- /// A task that represents the asynchronous operation.
- public Task UpdateRangeAsync(IEnumerable entities)
- {
- throw new NotImplementedException();
- }
-}
diff --git a/src/Altinn.Profile.Integrations/ServiceCollectionExtensions.cs b/src/Altinn.Profile.Integrations/ServiceCollectionExtensions.cs
index 05dae22..780b9d5 100644
--- a/src/Altinn.Profile.Integrations/ServiceCollectionExtensions.cs
+++ b/src/Altinn.Profile.Integrations/ServiceCollectionExtensions.cs
@@ -1,6 +1,12 @@
using System.Diagnostics.CodeAnalysis;
+using Altinn.ApiClients.Maskinporten.Extensions;
+using Altinn.ApiClients.Maskinporten.Services;
+using Altinn.Profile.Core.ContactRegister;
+
using Altinn.Profile.Core.Integrations;
+using Altinn.Profile.Core.Person.ContactPreferences;
+using Altinn.Profile.Integrations.ContactRegister;
using Altinn.Profile.Integrations.Extensions;
using Altinn.Profile.Integrations.Mappings;
using Altinn.Profile.Integrations.Persistence;
@@ -58,13 +64,41 @@ public static void AddRegisterService(this IServiceCollection services, IConfigu
throw new InvalidOperationException("Database connection string is not properly configured.");
}
- services.AddDbContext(options => options.UseNpgsql(connectionString));
-
- services.AddAutoMapper(typeof(PersonContactDetailsProfile));
-
services.AddScoped();
services.AddScoped();
+ services.AddScoped();
+
+ services.AddAutoMapper(typeof(PersonMappingProfile));
services.AddSingleton();
+
+ services.AddDbContextFactory(options => options.UseNpgsql(connectionString));
+ }
+
+ ///
+ /// Adds the Maskinporten client to the DI container.
+ ///
+ /// The service collection.
+ /// The configuration collection.
+ /// Thrown when the configuration is null.
+ /// Thrown when any of the required configuration values are missing or empty.
+ public static void AddMaskinportenClient(this IServiceCollection services, IConfiguration config)
+ {
+ if (config == null)
+ {
+ throw new ArgumentNullException(nameof(config), "Configuration cannot be null.");
+ }
+
+ var contactRegisterSettings = new ContactRegisterSettings();
+ config.GetSection("ContactAndReservationSettings").Bind(contactRegisterSettings);
+ if (contactRegisterSettings.MaskinportenSettings == null)
+ {
+ throw new InvalidOperationException("Contact and reservation settings are not properly configured.");
+ }
+
+ services.AddScoped();
+
+ services.AddSingleton(contactRegisterSettings);
+ services.AddMaskinportenHttpClient(contactRegisterSettings.MaskinportenSettings);
}
}
diff --git a/src/Altinn.Profile.Integrations/Services/IPersonService.cs b/src/Altinn.Profile.Integrations/Services/IPersonService.cs
index ad93f40..ec16f4e 100644
--- a/src/Altinn.Profile.Integrations/Services/IPersonService.cs
+++ b/src/Altinn.Profile.Integrations/Services/IPersonService.cs
@@ -11,20 +11,17 @@ namespace Altinn.Profile.Integrations.Services;
public interface IPersonService
{
///
- /// Asynchronously retrieves the contact details for a single person based on their national identity number.
+ /// Asynchronously retrieves the contact preferences for multiple persons based on their national identity numbers.
///
- /// The national identity number of the person.
+ /// A collection of national identity numbers.
///
- /// A task that represents the asynchronous operation. The task result contains the person's contact details, or null if not found.
+ /// A task that represents the asynchronous operation. The task result contains a object, where represents the successful lookup result and indicates a failure.
///
- Task GetContactDetailsAsync(string nationalIdentityNumber);
+ Task> GetContactPreferencesAsync(IEnumerable nationalIdentityNumbers);
///
- /// Asynchronously retrieves the contact details for multiple persons based on their national identity numbers.
+ /// Asynchronously synchronizes the person contact preferences.
///
- /// A collection of national identity numbers.
- ///
- /// A task that represents the asynchronous operation. The task result contains a object, where represents the successful lookup result and indicates a failure.
- ///
- Task> GetContactDetailsAsync(IEnumerable nationalIdentityNumbers);
+ /// A task that represents the asynchronous operation.
+ Task SyncPersonContactPreferencesAsync();
}
diff --git a/src/Altinn.Profile.Integrations/Services/NationalIdentityNumberChecker.cs b/src/Altinn.Profile.Integrations/Services/NationalIdentityNumberChecker.cs
index 816646d..b74db94 100644
--- a/src/Altinn.Profile.Integrations/Services/NationalIdentityNumberChecker.cs
+++ b/src/Altinn.Profile.Integrations/Services/NationalIdentityNumberChecker.cs
@@ -55,6 +55,6 @@ public IImmutableList GetValid(IEnumerable nationalIdentityNumbe
///
public bool IsValid(string nationalIdentityNumber)
{
- return nationalIdentityNumber.IsValidSocialSecurityNumber();
+ return nationalIdentityNumber.IsValidNationalIdentityNumber();
}
}
diff --git a/src/Altinn.Profile.Integrations/Services/PersonService.cs b/src/Altinn.Profile.Integrations/Services/PersonService.cs
index 0c2fd21..0af58eb 100644
--- a/src/Altinn.Profile.Integrations/Services/PersonService.cs
+++ b/src/Altinn.Profile.Integrations/Services/PersonService.cs
@@ -3,6 +3,8 @@
using System.Collections.Immutable;
using Altinn.Profile.Core;
+using Altinn.Profile.Core.ContactRegister;
+using Altinn.Profile.Core.Person.ContactPreferences;
using Altinn.Profile.Integrations.Entities;
using Altinn.Profile.Integrations.Repositories;
@@ -17,68 +19,94 @@ public class PersonService : IPersonService
{
private readonly IMapper _mapper;
private readonly IPersonRepository _personRepository;
+ private readonly IMetadataRepository _metadataRepository;
+ private readonly IContactRegisterService _changesLogService;
private readonly INationalIdentityNumberChecker _nationalIdentityNumberChecker;
///
/// Initializes a new instance of the class.
///
- /// The mapper used for object mapping.
+ /// The objects mapper.
/// The repository used for accessing the person data.
+ /// The service used for logging changes in contact preferences.
+ /// The repository used for accessing metadata.
/// The service used for checking the validity of national identity numbers.
- ///
- /// Thrown if , , or is null.
- ///
- public PersonService(IMapper mapper, IPersonRepository personRepository, INationalIdentityNumberChecker nationalIdentityNumberChecker)
+ public PersonService(
+ IMapper mapper,
+ IPersonRepository personRepository,
+ IContactRegisterService changesLogService,
+ IMetadataRepository metadataRepository,
+ INationalIdentityNumberChecker nationalIdentityNumberChecker)
{
- _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
- _personRepository = personRepository ?? throw new ArgumentNullException(nameof(personRepository));
- _nationalIdentityNumberChecker = nationalIdentityNumberChecker ?? throw new ArgumentNullException(nameof(nationalIdentityNumberChecker));
+ _mapper = mapper;
+ _personRepository = personRepository;
+ _changesLogService = changesLogService;
+ _metadataRepository = metadataRepository;
+ _nationalIdentityNumberChecker = nationalIdentityNumberChecker;
}
///
- /// Asynchronously retrieves the contact details for a single person based on their national identity number.
- ///
- /// The national identity number of the person.
- ///
- /// A task that represents the asynchronous operation. The task result contains the person's contact details, or null if not found.
- ///
- public async Task GetContactDetailsAsync(string nationalIdentityNumber)
- {
- if (!_nationalIdentityNumberChecker.IsValid(nationalIdentityNumber))
- {
- return null;
- }
-
- var personContactDetails = await _personRepository.GetContactDetailsAsync([nationalIdentityNumber]);
- return _mapper.Map(personContactDetails.FirstOrDefault());
- }
-
- ///
- /// Asynchronously retrieves the contact details for multiple persons based on their national identity numbers.
+ /// Asynchronously retrieves the contact preferences for multiple persons based on their national identity numbers.
///
/// A collection of national identity numbers.
///
- /// A task that represents the asynchronous operation. The task result contains a object, where represents the successful lookup result and indicates a failure.
+ /// A task that represents the asynchronous operation. The task result contains a
+ /// object, where represents the successful lookup result and indicates a failure.
///
/// Thrown if is null.
- public async Task> GetContactDetailsAsync(IEnumerable nationalIdentityNumbers)
+ public async Task> GetContactPreferencesAsync(IEnumerable nationalIdentityNumbers)
{
ArgumentNullException.ThrowIfNull(nationalIdentityNumbers);
var validNationalIdentityNumbers = _nationalIdentityNumberChecker.GetValid(nationalIdentityNumbers);
+ if (validNationalIdentityNumbers == null || validNationalIdentityNumbers.Count == 0)
+ {
+ return false;
+ }
- var matchedContactDetails = await _personRepository.GetContactDetailsAsync(validNationalIdentityNumbers);
-
- var matchedNationalIdentityNumbers = matchedContactDetails != null ? new HashSet(matchedContactDetails.Select(e => e.FnumberAk)) : [];
+ Result, bool> matchedContactDetails = await _personRepository.GetContactDetailsAsync(validNationalIdentityNumbers);
- var unmatchedNationalIdentityNumbers = nationalIdentityNumbers.Where(e => !matchedNationalIdentityNumbers.Contains(e));
+ HashSet matchedNationalIdentityNumbers = [];
+ IEnumerable unmatchedNationalIdentityNumbers = [];
+ IEnumerable matchedPersonContactDetails = [];
- var matchedPersonContactDetails = matchedContactDetails != null ? matchedContactDetails.Select(_mapper.Map) : [];
+ matchedContactDetails.Match(
+ e =>
+ {
+ if (e is not null && e.Count > 0)
+ {
+ matchedNationalIdentityNumbers = new HashSet(e.Select(e => e.FnumberAk));
+ matchedPersonContactDetails = e.Select(_mapper.Map).ToImmutableList();
+ unmatchedNationalIdentityNumbers = nationalIdentityNumbers.Where(e => !matchedNationalIdentityNumbers.Contains(e));
+ }
+ },
+ _ => { });
- return new PersonContactDetailsLookupResult
+ return new PersonContactPreferencesLookupResult
{
- MatchedPersonContactDetails = matchedPersonContactDetails.ToImmutableList(),
- UnmatchedNationalIdentityNumbers = unmatchedNationalIdentityNumbers.ToImmutableList()
+ MatchedPersonContactPreferences = matchedPersonContactDetails.Any() ? matchedPersonContactDetails.ToImmutableList() : null,
+ UnmatchedNationalIdentityNumbers = unmatchedNationalIdentityNumbers.Any() ? unmatchedNationalIdentityNumbers.ToImmutableList() : null
};
}
+
+ ///
+ /// Asynchronously synchronizes the person contact preferences.
+ ///
+ /// A task that represents the asynchronous operation.
+ public async Task SyncPersonContactPreferencesAsync()
+ {
+ // Get the latest change number.
+ long latestChangeNumber = 0;
+ Result latestChangeNumberGetter = await _metadataRepository.GetLatestChangeNumberAsync();
+ latestChangeNumberGetter.Match(e => latestChangeNumber = e, _ => latestChangeNumber = 0);
+
+ // Retrieve the changes in contact preferences from the changes log.
+ ContactRegisterChangesLog contactDetailsChanges = await _changesLogService.RetrieveContactDetailsChangesAsync(latestChangeNumber);
+
+ int synchornizedRowCount = await _personRepository.SyncPersonContactPreferencesAsync(contactDetailsChanges);
+ if (synchornizedRowCount > 0 && contactDetailsChanges.EndingIdentifier.HasValue)
+ {
+ await _metadataRepository.UpdateLatestChangeNumberAsync(contactDetailsChanges.EndingIdentifier.Value);
+ }
+ }
}
diff --git a/src/Altinn.Profile/Controllers/ContactDetailsController.cs b/src/Altinn.Profile/Controllers/PersonContactDetailsController.cs
similarity index 51%
rename from src/Altinn.Profile/Controllers/ContactDetailsController.cs
rename to src/Altinn.Profile/Controllers/PersonContactDetailsController.cs
index 8a5e235..e9bc6a9 100644
--- a/src/Altinn.Profile/Controllers/ContactDetailsController.cs
+++ b/src/Altinn.Profile/Controllers/PersonContactDetailsController.cs
@@ -12,31 +12,24 @@
namespace Altinn.Profile.Controllers;
///
-/// Controller to retrieve the contact details for one or more persons.
+/// Controller responsible for managing contact details for one or more persons.
///
+///
+/// Initializes a new instance of the class.
+///
+/// The logger instance used for logging.
+/// The service for retrieving the contact details.
[Authorize]
[ApiController]
[Consumes("application/json")]
[Produces("application/json")]
-[Route("profile/api/v1/contact/details")]
-public class ContactDetailsController : ControllerBase
+[Route("profile/api/v1/person/contact/details")]
+public class PersonContactDetailsController(
+ ILogger logger, IPersonContactDetailsRetriever contactDetailsRetriever)
+ : ControllerBase
{
- private readonly ILogger _logger;
- private readonly IContactDetailsRetriever _contactDetailsRetriever;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The logger instance used for logging.
- /// The use case for retrieving the contact details.
- ///
- /// Thrown when the or is null.
- ///
- public ContactDetailsController(ILogger logger, IContactDetailsRetriever contactDetailsRetriever)
- {
- _logger = logger ?? throw new ArgumentNullException(nameof(logger));
- _contactDetailsRetriever = contactDetailsRetriever ?? throw new ArgumentNullException(nameof(contactDetailsRetriever));
- }
+ private readonly IPersonContactDetailsRetriever _contactDetailsRetriever = contactDetailsRetriever;
+ private readonly ILogger _logger = logger;
///
/// Retrieves the contact details for persons based on their national identity numbers.
@@ -44,13 +37,13 @@ public ContactDetailsController(ILogger logger, IConta
/// A collection of national identity numbers.
///
/// A task that represents the asynchronous operation, containing a response with persons' contact details.
- /// Returns a with status 200 OK if successful.
+ /// Returns a with status 200 OK if successful.
///
[HttpPost("lookup")]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
- [ProducesResponseType(typeof(ContactDetailsLookupResult), StatusCodes.Status200OK)]
- public async Task> PostLookup([FromBody] UserContactPointLookup lookupCriteria)
+ [ProducesResponseType(typeof(PersonContactDetailsLookupResult), StatusCodes.Status200OK)]
+ public async Task> PostLookup([FromBody] UserContactDetailsLookupCriteria lookupCriteria)
{
if (!ModelState.IsValid)
{
@@ -66,10 +59,10 @@ public async Task> PostLookup([FromBody
{
var lookupResult = await _contactDetailsRetriever.RetrieveAsync(lookupCriteria);
- return lookupResult.Match>(
+ return lookupResult.Match>(
successResponse =>
{
- return successResponse?.MatchedContactDetails?.Count > 0 ? Ok(successResponse) : NotFound();
+ return successResponse?.MatchedPersonContactDetails?.Count > 0 ? Ok(successResponse) : NotFound();
},
failedResponse => NotFound());
}
diff --git a/src/Altinn.Profile/Controllers/ContactDetailsInternalController.cs b/src/Altinn.Profile/Controllers/PersonContactDetailsInternalController.cs
similarity index 62%
rename from src/Altinn.Profile/Controllers/ContactDetailsInternalController.cs
rename to src/Altinn.Profile/Controllers/PersonContactDetailsInternalController.cs
index 1209689..1662a92 100644
--- a/src/Altinn.Profile/Controllers/ContactDetailsInternalController.cs
+++ b/src/Altinn.Profile/Controllers/PersonContactDetailsInternalController.cs
@@ -18,22 +18,21 @@ namespace Altinn.Profile.Controllers;
[Consumes("application/json")]
[Produces("application/json")]
[ApiExplorerSettings(IgnoreApi = true)]
-[Route("profile/api/v1/internal/contact/details")]
-public class ContactDetailsInternalController : ControllerBase
+[Route("profile/api/v1/internal/person/contact/details")]
+public class PersonContactDetailsInternalController : ControllerBase
{
- private readonly ILogger _logger;
- private readonly IContactDetailsRetriever _contactDetailsRetriever;
+ private readonly IPersonContactDetailsRetriever _contactDetailsRetriever;
+ private readonly ILogger _logger;
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
/// The logger instance used for logging.
/// The use case for retrieving the contact details.
- /// Thrown when the is null.
- public ContactDetailsInternalController(ILogger logger, IContactDetailsRetriever contactDetailsRetriever)
+ public PersonContactDetailsInternalController(ILogger logger, IPersonContactDetailsRetriever contactDetailsRetriever)
{
- _logger = logger ?? throw new ArgumentNullException(nameof(logger));
- _contactDetailsRetriever = contactDetailsRetriever ?? throw new ArgumentNullException(nameof(contactDetailsRetriever));
+ _logger = logger;
+ _contactDetailsRetriever = contactDetailsRetriever;
}
///
@@ -42,14 +41,14 @@ public ContactDetailsInternalController(ILoggerA collection of national identity numbers.
///
/// A task that represents the asynchronous operation, containing a response with persons' contact details.
- /// Returns a with status 200 OK if successful,
+ /// Returns a with status 200 OK if successful,
/// 400 Bad Request if the request is invalid, or 404 Not Found if no contact details are found.
///
[HttpPost("lookup")]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
- [ProducesResponseType(typeof(ContactDetailsLookupResult), StatusCodes.Status200OK)]
- public async Task> PostLookup([FromBody] UserContactPointLookup request)
+ [ProducesResponseType(typeof(PersonContactDetailsLookupResult), StatusCodes.Status200OK)]
+ public async Task> PostLookup([FromBody] UserContactDetailsLookupCriteria request)
{
if (!ModelState.IsValid)
{
@@ -65,10 +64,10 @@ public async Task> PostLookup([FromBody
{
var result = await _contactDetailsRetriever.RetrieveAsync(request);
- return result.Match>(
+ return result.Match>(
success =>
{
- return success?.MatchedContactDetails?.Count > 0 ? Ok(success) : NotFound();
+ return success?.MatchedPersonContactDetails?.Count > 0 ? Ok(success) : NotFound();
},
noMatch => NotFound());
}
diff --git a/src/Altinn.Profile/Controllers/TriggerController.cs b/src/Altinn.Profile/Controllers/TriggerController.cs
new file mode 100644
index 0000000..74d5e2d
--- /dev/null
+++ b/src/Altinn.Profile/Controllers/TriggerController.cs
@@ -0,0 +1,49 @@
+using System;
+using System.Threading.Tasks;
+
+using Altinn.Profile.Integrations.Services;
+
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Altinn.Profile.Controllers;
+
+///
+/// Controller responsible for managing changes in contact preferences for one or more persons.
+///
+[ApiController]
+[ApiExplorerSettings(IgnoreApi = true)]
+[Consumes("application/json")]
+[Produces("application/json")]
+[Route("profile/api/v1/trigger")]
+public class TriggerController : ControllerBase
+{
+ private readonly IPersonService _personService;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The service for retrieving the contact details.
+ public TriggerController(IPersonService personService)
+ {
+ _personService = personService;
+ }
+
+ ///
+ /// Synchronizes the changes in the contact details for persons.
+ ///
+ ///
+ /// A task that represents the asynchronous operation. If successful, returns a status 200 OK.
+ ///
+ /// Starting the synchronisation work was successfull.
+ /// Returns a problem detail if an unexpected error occurs.
+ [HttpGet("syncpersonchanges")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public async Task SyncChanges()
+ {
+ await _personService.SyncPersonContactPreferencesAsync();
+
+ return Ok();
+ }
+}
diff --git a/src/Altinn.Profile/Controllers/UserContactPointController.cs b/src/Altinn.Profile/Controllers/UserContactPointController.cs
index d45d328..f150943 100644
--- a/src/Altinn.Profile/Controllers/UserContactPointController.cs
+++ b/src/Altinn.Profile/Controllers/UserContactPointController.cs
@@ -12,6 +12,7 @@ namespace Altinn.Profile.Controllers;
///
/// Controller for user profile contact point API endpoints for internal consumption (e.g. Notifications) requiring neither authenticated user token nor access token authorization.
///
+[ApiController]
[Route("profile/api/v1/users/contactpoint")]
[ApiExplorerSettings(IgnoreApi = true)]
[Consumes("application/json")]
@@ -34,7 +35,7 @@ public UserContactPointController(IUserContactPoints contactPointService)
/// Returns an overview of the availability of various contact points for the user
[HttpPost("availability")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public async Task> PostAvailabilityLookup([FromBody] UserContactPointLookup userContactPointLookup)
+ public async Task> PostAvailabilityLookup([FromBody] UserContactDetailsLookupCriteria userContactPointLookup)
{
if (userContactPointLookup.NationalIdentityNumbers.Count == 0)
{
@@ -54,7 +55,7 @@ public async Task> PostAvailabili
/// Returns an overview of the contact points for the user
[HttpPost("lookup")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public async Task> PostLookup([FromBody] UserContactPointLookup userContactPointLookup)
+ public async Task> PostLookup([FromBody] UserContactDetailsLookupCriteria userContactPointLookup)
{
Result result = await _contactPointService.GetContactPoints(userContactPointLookup.NationalIdentityNumbers);
return result.Match>(
diff --git a/src/Altinn.Profile/Models/ContactDetails.cs b/src/Altinn.Profile/Models/PersonContactDetails.cs
similarity index 79%
rename from src/Altinn.Profile/Models/ContactDetails.cs
rename to src/Altinn.Profile/Models/PersonContactDetails.cs
index bba8c59..09c72e6 100644
--- a/src/Altinn.Profile/Models/ContactDetails.cs
+++ b/src/Altinn.Profile/Models/PersonContactDetails.cs
@@ -5,37 +5,37 @@
namespace Altinn.Profile.Models;
///
-/// Represents the contact information for a single person, including national identity number, contact methods, language preference, and opt-out status.
+/// Represents the contact details for a single person, including the national identity number, mobile phone number, email address, language preference, and an opt-out status for being contacted.
///
-public record ContactDetails
+public record PersonContactDetails
{
///
- /// Gets the national identity number of the person.
+ /// Gets the email address of the person.
///
- [JsonPropertyName("nationalIdentityNumber")]
- public required string NationalIdentityNumber { get; init; }
+ [JsonPropertyName("emailAddress")]
+ public string? EmailAddress { get; init; }
///
/// Gets a value indicating whether the person has opted out of being contacted.
///
[JsonPropertyName("reservation")]
- public bool? Reservation { get; init; }
+ public bool? IsReserved { get; init; }
///
- /// Gets the mobile phone number of the person.
+ /// Gets the language code preferred by the person for communication.
///
- [JsonPropertyName("mobilePhoneNumber")]
- public string? MobilePhoneNumber { get; init; }
+ [JsonPropertyName("languageCode")]
+ public string? LanguageCode { get; init; }
///
- /// Gets the email address of the person.
+ /// Gets the mobile phone number of the person.
///
- [JsonPropertyName("emailAddress")]
- public string? EmailAddress { get; init; }
+ [JsonPropertyName("mobilePhoneNumber")]
+ public string? MobilePhoneNumber { get; init; }
///
- /// Gets the language code preferred by the person for communication.
+ /// Gets the national identity number of the person.
///
- [JsonPropertyName("languageCode")]
- public string? LanguageCode { get; init; }
+ [JsonPropertyName("nationalIdentityNumber")]
+ public required string NationalIdentityNumber { get; init; }
}
diff --git a/src/Altinn.Profile/Models/ContactDetailsLookupResult.cs b/src/Altinn.Profile/Models/PersonContactDetailsLookupResult.cs
similarity index 53%
rename from src/Altinn.Profile/Models/ContactDetailsLookupResult.cs
rename to src/Altinn.Profile/Models/PersonContactDetailsLookupResult.cs
index 1a9d4a6..c6650f2 100644
--- a/src/Altinn.Profile/Models/ContactDetailsLookupResult.cs
+++ b/src/Altinn.Profile/Models/PersonContactDetailsLookupResult.cs
@@ -6,16 +6,29 @@
namespace Altinn.Profile.Models;
///
-/// Represents the results of a contact details lookup operation.
+/// Represents the result of a contact details lookup operation for one or more persons.
///
-public record ContactDetailsLookupResult
+public record PersonContactDetailsLookupResult
{
///
- /// Gets a list of contact details that were successfully matched based on the national identity number.
+ /// Initializes a new instance of the record.
///
- [JsonPropertyName("matchedContactDetails")]
+ /// The list of person contact details that were successfully matched based on the national identity number.
+ /// The list of national identity numbers that could not be matched with any contact details.
+ public PersonContactDetailsLookupResult(
+ ImmutableList matchedPersonContactDetails,
+ ImmutableList unmatchedNationalIdentityNumbers)
+ {
+ MatchedPersonContactDetails = matchedPersonContactDetails;
+ UnmatchedNationalIdentityNumbers = unmatchedNationalIdentityNumbers;
+ }
+
+ ///
+ /// Gets a list of person contact details that were successfully matched based on the national identity number.
+ ///
+ [JsonPropertyName("matchedPersonContactDetails")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
- public ImmutableList? MatchedContactDetails { get; init; }
+ public ImmutableList? MatchedPersonContactDetails { get; init; }
///
/// Gets a list of national identity numbers that could not be matched with any contact details.
@@ -23,17 +36,4 @@ public record ContactDetailsLookupResult
[JsonPropertyName("unmatchedNationalIdentityNumbers")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public ImmutableList? UnmatchedNationalIdentityNumbers { get; init; }
-
- ///
- /// Initializes a new instance of the record.
- ///
- /// The list of contact details that were successfully matched based on the national identity number.
- /// The list of national identity numbers that could not be matched with any contact details.
- public ContactDetailsLookupResult(
- ImmutableList matchedContactDetails,
- ImmutableList unmatchedNationalIdentityNumbers)
- {
- MatchedContactDetails = matchedContactDetails;
- UnmatchedNationalIdentityNumbers = unmatchedNationalIdentityNumbers;
- }
}
diff --git a/src/Altinn.Profile/Models/UserContactPointLookup.cs b/src/Altinn.Profile/Models/UserContactDetailsLookupCriteria.cs
similarity index 72%
rename from src/Altinn.Profile/Models/UserContactPointLookup.cs
rename to src/Altinn.Profile/Models/UserContactDetailsLookupCriteria.cs
index 8ee6bb4..9f4982c 100644
--- a/src/Altinn.Profile/Models/UserContactPointLookup.cs
+++ b/src/Altinn.Profile/Models/UserContactDetailsLookupCriteria.cs
@@ -3,9 +3,9 @@
namespace Altinn.Profile.Models;
///
-/// A class representing a user contact point lookup object.
+/// Represents the lookup criteria to retrieve the contact details for one or more persons.
///
-public class UserContactPointLookup
+public class UserContactDetailsLookupCriteria
{
///
/// A collection of national identity numbers used to retrieve contact points, obtain contact details, or check the availability of contact points.
diff --git a/src/Altinn.Profile/Program.cs b/src/Altinn.Profile/Program.cs
index 2f97e5d..f061781 100644
--- a/src/Altinn.Profile/Program.cs
+++ b/src/Altinn.Profile/Program.cs
@@ -57,7 +57,7 @@
WebApplication app = builder.Build();
-app.SetUpPostgreSql(builder.Environment.IsDevelopment(), builder.Configuration);
+app.SetUpPostgreSql(builder.Configuration);
Configure();
@@ -162,7 +162,7 @@ void ConfigureServices(IServiceCollection services, IConfiguration config)
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
- services.AddScoped();
+ services.AddScoped();
services.AddAuthentication(JwtCookieDefaults.AuthenticationScheme)
.AddJwtCookie(JwtCookieDefaults.AuthenticationScheme, options =>
@@ -194,6 +194,7 @@ void ConfigureServices(IServiceCollection services, IConfiguration config)
services.AddCoreServices(config);
services.AddRegisterService(config);
services.AddSblBridgeClients(config);
+ services.AddMaskinportenClient(config);
if (!string.IsNullOrEmpty(applicationInsightsConnectionString))
{
diff --git a/src/Altinn.Profile/UseCases/ContactDetailsRetriever.cs b/src/Altinn.Profile/UseCases/ContactDetailsRetriever.cs
deleted file mode 100644
index f352262..0000000
--- a/src/Altinn.Profile/UseCases/ContactDetailsRetriever.cs
+++ /dev/null
@@ -1,91 +0,0 @@
-using System;
-using System.Collections.Immutable;
-using System.Linq;
-using System.Threading.Tasks;
-
-using Altinn.Profile.Core;
-using Altinn.Profile.Integrations.Entities;
-using Altinn.Profile.Integrations.Services;
-using Altinn.Profile.Models;
-
-namespace Altinn.Profile.UseCases;
-
-///
-/// Provides an implementation for retrieving the contact details for one or more persons.
-///
-public class ContactDetailsRetriever : IContactDetailsRetriever
-{
- private readonly IPersonService _personService;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The person service for retrieving contact details.
- /// Thrown when is null.
- public ContactDetailsRetriever(IPersonService personService)
- {
- _personService = personService ?? throw new ArgumentNullException(nameof(personService));
- }
-
- ///
- /// Asynchronously retrieves the contact details for one or more persons based on the specified lookup criteria.
- ///
- /// The criteria used to look up contact details, including the national identity numbers of the persons.
- ///
- /// A task representing the asynchronous operation.
- /// The task result contains a object, where represents the successful outcome and indicates a failure.
- ///
- /// Thrown when is null.
- public async Task> RetrieveAsync(UserContactPointLookup lookupCriteria)
- {
- ArgumentNullException.ThrowIfNull(lookupCriteria);
-
- if (lookupCriteria.NationalIdentityNumbers == null || lookupCriteria.NationalIdentityNumbers.Count == 0)
- {
- return false;
- }
-
- var contactDetails = await _personService.GetContactDetailsAsync(lookupCriteria.NationalIdentityNumbers);
-
- return contactDetails.Match(
- MapToContactDetailsLookupResult,
- _ => false);
- }
-
- ///
- /// Maps the person contact details to a .
- ///
- /// The person contact details to map.
- /// The mapped .
- /// Thrown when is null.
- private ContactDetails MapToContactDetails(IPersonContactDetails contactDetails)
- {
- ArgumentNullException.ThrowIfNull(contactDetails);
-
- return new ContactDetails
- {
- Reservation = contactDetails.IsReserved,
- EmailAddress = contactDetails.EmailAddress,
- LanguageCode = contactDetails.LanguageCode,
- MobilePhoneNumber = contactDetails.MobilePhoneNumber,
- NationalIdentityNumber = contactDetails.NationalIdentityNumber
- };
- }
-
- ///
- /// Maps the person contact details lookup result to a .
- ///
- /// The lookup result containing the person contact details.
- ///
- /// A containing a if the mapping is successful, or false if the mapping fails.
- ///
- /// Thrown when is null.
- private Result MapToContactDetailsLookupResult(IPersonContactDetailsLookupResult lookupResult)
- {
- ArgumentNullException.ThrowIfNull(lookupResult);
-
- var matchedContactDetails = lookupResult.MatchedPersonContactDetails?.Select(MapToContactDetails).ToImmutableList();
-
- return new ContactDetailsLookupResult(matchedContactDetails, lookupResult.UnmatchedNationalIdentityNumbers);
- }
-}
diff --git a/src/Altinn.Profile/UseCases/IContactDetailsRetriever.cs b/src/Altinn.Profile/UseCases/IPersonContactDetailsRetriever.cs
similarity index 62%
rename from src/Altinn.Profile/UseCases/IContactDetailsRetriever.cs
rename to src/Altinn.Profile/UseCases/IPersonContactDetailsRetriever.cs
index b3ff503..ac730c8 100644
--- a/src/Altinn.Profile/UseCases/IContactDetailsRetriever.cs
+++ b/src/Altinn.Profile/UseCases/IPersonContactDetailsRetriever.cs
@@ -8,15 +8,15 @@ namespace Altinn.Profile.UseCases;
///
/// Defines a use case for retrieving the contact details for one or more persons.
///
-public interface IContactDetailsRetriever
+public interface IPersonContactDetailsRetriever
{
///
/// Asynchronously retrieves the contact details for one or more persons based on the specified lookup criteria.
///
- /// The criteria used to look up contact details, including the national identity numbers of the persons.
+ /// The criteria used to look up contact .
///
/// A task representing the asynchronous operation.
- /// The task result contains a object, where represents the successful outcome and indicates a failure.
+ /// The task result contains a object, where represents the successful outcome and indicates a failure.
///
- Task> RetrieveAsync(UserContactPointLookup lookupCriteria);
+ Task> RetrieveAsync(UserContactDetailsLookupCriteria lookupCriteria);
}
diff --git a/src/Altinn.Profile/UseCases/PersonContactDetailsRetriever.cs b/src/Altinn.Profile/UseCases/PersonContactDetailsRetriever.cs
new file mode 100644
index 0000000..7237204
--- /dev/null
+++ b/src/Altinn.Profile/UseCases/PersonContactDetailsRetriever.cs
@@ -0,0 +1,86 @@
+using System;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Threading.Tasks;
+
+using Altinn.Profile.Core;
+using Altinn.Profile.Core.Person.ContactPreferences;
+using Altinn.Profile.Integrations.Entities;
+using Altinn.Profile.Integrations.Services;
+using Altinn.Profile.Models;
+
+namespace Altinn.Profile.UseCases;
+
+///
+/// Provides an implementation for retrieving the contact details for one or more persons.
+///
+///
+/// Initializes a new instance of the class.
+///
+/// The person service for retrieving contact details.
+public class PersonContactDetailsRetriever(IPersonService personService) : IPersonContactDetailsRetriever
+{
+ private readonly IPersonService _personService = personService;
+
+ ///
+ /// Asynchronously retrieves the contact details for one or more persons based on the specified lookup criteria.
+ ///
+ /// The criteria used to look up contact details.
+ ///
+ /// A task representing the asynchronous operation.
+ /// The task result contains a object, where represents the successful outcome and indicates a failure.
+ ///
+ /// Thrown when is null.
+ public async Task> RetrieveAsync(UserContactDetailsLookupCriteria lookupCriteria)
+ {
+ ArgumentNullException.ThrowIfNull(lookupCriteria);
+
+ if (lookupCriteria.NationalIdentityNumbers == null || lookupCriteria.NationalIdentityNumbers.Count == 0)
+ {
+ return false;
+ }
+
+ var contactDetails = await _personService.GetContactPreferencesAsync(lookupCriteria.NationalIdentityNumbers);
+
+ return contactDetails.Match(
+ MapToContactDetailsLookupResult,
+ _ => false);
+ }
+
+ ///
+ /// Maps the person contact details to a .
+ ///
+ /// The person contact details to map.
+ /// The mapped .
+ /// Thrown when is null.
+ private PersonContactDetails MapToContactDetails(PersonContactPreferences contactPreferences)
+ {
+ ArgumentNullException.ThrowIfNull(contactPreferences);
+
+ return new PersonContactDetails
+ {
+ IsReserved = contactPreferences.IsReserved,
+ EmailAddress = contactPreferences.Email,
+ LanguageCode = contactPreferences.LanguageCode,
+ MobilePhoneNumber = contactPreferences.MobileNumber,
+ NationalIdentityNumber = contactPreferences.NationalIdentityNumber
+ };
+ }
+
+ ///
+ /// Maps the person contact details lookup result to a .
+ ///
+ /// The lookup result containing the person contact details.
+ ///
+ /// A containing a if the mapping is successful, or false if the mapping fails.
+ ///
+ /// Thrown when is null.
+ private Result MapToContactDetailsLookupResult(IPersonContactPreferencesLookupResult lookupResult)
+ {
+ ArgumentNullException.ThrowIfNull(lookupResult);
+
+ var matchedContactDetails = lookupResult.MatchedPersonContactPreferences?.Select(MapToContactDetails).ToImmutableList();
+
+ return new PersonContactDetailsLookupResult(matchedContactDetails, lookupResult.UnmatchedNationalIdentityNumbers);
+ }
+}
diff --git a/src/Altinn.Profile/appsettings.Development.json b/src/Altinn.Profile/appsettings.Development.json
index 01d01ec..a2ac3a3 100644
--- a/src/Altinn.Profile/appsettings.Development.json
+++ b/src/Altinn.Profile/appsettings.Development.json
@@ -1,6 +1,6 @@
{
"PostgreSqlSettings": {
- "MigrationScriptPath": "Altinn.Profile.Integrations/Migration"
+ "MigrationScriptPath": "../Altinn.Profile.Integrations/Migration"
},
"Logging": {
"LogLevel": {
diff --git a/src/Altinn.Profile/appsettings.json b/src/Altinn.Profile/appsettings.json
index e919f04..8add802 100644
--- a/src/Altinn.Profile/appsettings.json
+++ b/src/Altinn.Profile/appsettings.json
@@ -16,5 +16,14 @@
"ProfileDbAdminPwd": "Password",
"ProfileDbPwd": "Password",
"EnableDBConnection": true
+ },
+ "ContactAndReservationSettings": {
+ "ChangesLogEndpoint": "https://test.kontaktregisteret.no/rest/v2/krr/hentEndringer",
+ "MaskinportenSettings": {
+ "Environment": "test",
+ "Scope": "krr:global/digitalpost.read krr:global/kontaktinformasjon.read krr:global/hentendring.read",
+ "ClientId": "value injected as secret",
+ "EncodedJwk": "value injected as secret"
+ }
}
}
diff --git a/test/Altinn.Profile.Tests/Altinn.Profile.Tests.csproj b/test/Altinn.Profile.Tests/Altinn.Profile.Tests.csproj
index ec727d7..19fed50 100644
--- a/test/Altinn.Profile.Tests/Altinn.Profile.Tests.csproj
+++ b/test/Altinn.Profile.Tests/Altinn.Profile.Tests.csproj
@@ -71,6 +71,9 @@
Always
+
+ PreserveNewest
+
diff --git a/test/Altinn.Profile.Tests/IntegrationTests/API/Controllers/ContactDetailsControllerTests.cs b/test/Altinn.Profile.Tests/IntegrationTests/API/Controllers/ContactDetailsControllerTests.cs
deleted file mode 100644
index ef8ebe0..0000000
--- a/test/Altinn.Profile.Tests/IntegrationTests/API/Controllers/ContactDetailsControllerTests.cs
+++ /dev/null
@@ -1,312 +0,0 @@
-using System;
-using System.Threading.Tasks;
-
-using Altinn.Profile.Controllers;
-using Altinn.Profile.Models;
-using Altinn.Profile.UseCases;
-
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Logging;
-
-using Moq;
-
-using Xunit;
-
-namespace Altinn.Profile.Tests.IntegrationTests.API.Controllers;
-
-public class ContactDetailsControllerTests
-{
- private readonly ContactDetailsController _controller;
- private readonly Mock> _loggerMock;
- private readonly Mock _mockContactDetailsRetriever;
-
- public ContactDetailsControllerTests()
- {
- _loggerMock = new Mock>();
- _mockContactDetailsRetriever = new Mock();
- _controller = new ContactDetailsController(_loggerMock.Object, _mockContactDetailsRetriever.Object);
- }
-
- [Fact]
- public void Constructor_WithNullContactDetailsRetriever_ThrowsArgumentNullException()
- {
- // Arrange
- var loggerMock = new Mock>();
-
- // Act & Assert
- Assert.Throws(() => new ContactDetailsController(loggerMock.Object, null));
- }
-
- [Fact]
- public void Constructor_WithNullLogger_ThrowsArgumentNullException()
- {
- // Arrange
- var contactDetailsRetrieverMock = new Mock();
-
- // Act & Assert
- Assert.Throws(() => new ContactDetailsController(null, contactDetailsRetrieverMock.Object));
- }
-
- [Fact]
- public void Constructor_WithValidParameters_InitializesCorrectly()
- {
- // Arrange
- var loggerMock = new Mock>();
- var contactDetailsRetrieverMock = new Mock();
-
- // Act
- var controller = new ContactDetailsController(loggerMock.Object, contactDetailsRetrieverMock.Object);
-
- // Assert
- Assert.NotNull(controller);
- }
-
- [Fact]
- public async Task PostLookup_WhenExceptionOccurs_ReturnsInternalServerError_And_LogsError()
- {
- // Arrange
- var request = new UserContactPointLookup { NationalIdentityNumbers = { "27038893837" } };
- _mockContactDetailsRetriever.Setup(x => x.RetrieveAsync(request)).ThrowsAsync(new Exception("Test exception"));
-
- // Act
- var response = await _controller.PostLookup(request);
-
- // Assert
- var problemResult = Assert.IsType(response.Result);
- Assert.Equal(StatusCodes.Status500InternalServerError, problemResult.StatusCode);
-
- _loggerMock.Verify(
- x => x.Log(
- LogLevel.Error,
- It.IsAny(),
- It.Is((v, t) => v.ToString().Contains("An error occurred while retrieving contact details.")),
- It.IsAny(),
- It.IsAny>()),
- Times.Once);
- }
-
- [Fact]
- public async Task PostLookup_WhenLookupCriteriaIsNull_ReturnsBadRequest()
- {
- // Arrange
- UserContactPointLookup request = null;
-
- // Act
- var response = await _controller.PostLookup(request);
-
- // Assert
- var badRequestResult = Assert.IsType(response.Result);
- Assert.Equal(StatusCodes.Status400BadRequest, badRequestResult.StatusCode);
- Assert.Equal("National identity numbers cannot be null or empty.", badRequestResult.Value);
- }
-
- [Fact]
- public async Task PostLookup_WhenMatchedContactDetailsAreFound_ReturnsOk()
- {
- // Arrange
- var request = new UserContactPointLookup { NationalIdentityNumbers = { "05053423096" } };
- var contactDetails = new ContactDetails
- {
- LanguageCode = "en",
- Reservation = false,
- MobilePhoneNumber = "98765432",
- EmailAddress = "user@example.com",
- NationalIdentityNumber = "05053423096"
- };
- var lookupResult = new ContactDetailsLookupResult(matchedContactDetails: [contactDetails], unmatchedNationalIdentityNumbers: []);
-
- _mockContactDetailsRetriever.Setup(x => x.RetrieveAsync(request)).ReturnsAsync(lookupResult);
-
- // Act
- var response = await _controller.PostLookup(request);
-
- // Assert
- var result = Assert.IsType(response.Result);
- Assert.Equal(StatusCodes.Status200OK, result.StatusCode);
-
- var returnValue = Assert.IsType(result.Value);
- Assert.NotNull(returnValue);
- Assert.Single(returnValue.MatchedContactDetails);
- Assert.Empty(returnValue.UnmatchedNationalIdentityNumbers);
- }
-
- [Fact]
- public async Task PostLookup_WhenNationalIdentityNumbersIsNull_ReturnsBadRequest()
- {
- // Arrange
- var request = new UserContactPointLookup { NationalIdentityNumbers = null };
-
- // Act
- var response = await _controller.PostLookup(request);
-
- // Assert
- var badRequestResult = Assert.IsType(response.Result);
- Assert.Equal(StatusCodes.Status400BadRequest, badRequestResult.StatusCode);
- Assert.Equal("National identity numbers cannot be null or empty.", badRequestResult.Value);
- }
-
- [Fact]
- public async Task PostLookup_WhenNoContactDetailsFound_ReturnsNotFound()
- {
- // Arrange
- var request = new UserContactPointLookup { NationalIdentityNumbers = { "30083542175" } };
- var lookupResult = new ContactDetailsLookupResult(matchedContactDetails: [], unmatchedNationalIdentityNumbers: ["30083542175"]);
-
- _mockContactDetailsRetriever.Setup(x => x.RetrieveAsync(request)).ReturnsAsync(lookupResult);
-
- // Act
- var response = await _controller.PostLookup(request);
-
- // Assert
- var notFoundResult = Assert.IsType(response.Result);
- Assert.Equal(StatusCodes.Status404NotFound, notFoundResult.StatusCode);
- }
-
- [Fact]
- public async Task PostLookup_WhenNoMatchedContactDetailsAreFound_ReturnsNotFound()
- {
- // Arrange
- var request = new UserContactPointLookup { NationalIdentityNumbers = { "99020312345" } };
- var lookupResult = new ContactDetailsLookupResult(matchedContactDetails: [], unmatchedNationalIdentityNumbers: ["99020312345"]);
-
- _mockContactDetailsRetriever.Setup(x => x.RetrieveAsync(request)).ReturnsAsync(lookupResult);
-
- // Act
- var response = await _controller.PostLookup(request);
-
- // Assert
- var notFoundResult = Assert.IsType(response.Result);
- Assert.Equal(StatusCodes.Status404NotFound, notFoundResult.StatusCode);
- }
-
- [Fact]
- public async Task PostLookup_WhenServiceCallIsLongRunning_DoesNotBlock()
- {
- // Arrange
- var request = new UserContactPointLookup { NationalIdentityNumbers = { "02038112735" } };
- var contactDetails = new ContactDetails
- {
- LanguageCode = "nb",
- Reservation = false,
- MobilePhoneNumber = "12345678",
- EmailAddress = "test@example.com",
- NationalIdentityNumber = "02038112735"
- };
- var lookupResult = new ContactDetailsLookupResult(matchedContactDetails: [contactDetails], unmatchedNationalIdentityNumbers: []);
-
- _mockContactDetailsRetriever.Setup(x => x.RetrieveAsync(request)).ReturnsAsync(() =>
- {
- Task.Delay(5000).Wait(); // Simulate long-running task
- return lookupResult;
- });
-
- // Act
- var response = await _controller.PostLookup(request);
-
- // Assert
- var result = Assert.IsType(response.Result);
- Assert.Equal(StatusCodes.Status200OK, result.StatusCode);
- Assert.Equal(lookupResult, Assert.IsType(result.Value));
- }
-
- [Fact]
- public async Task PostLookup_WithInvalidModelState_ReturnsBadRequest()
- {
- // Arrange
- var invalidRequest = new UserContactPointLookup { NationalIdentityNumbers = { "14078112078" } };
- _controller.ModelState.AddModelError("InvalidKey", "Invalid error message");
-
- // Act
- var response = await _controller.PostLookup(invalidRequest);
-
- // Assert
- var badRequestResult = Assert.IsType(response.Result);
- Assert.Equal(StatusCodes.Status400BadRequest, badRequestResult.StatusCode);
- }
-
- [Fact]
- public async Task PostLookup_WithInvalidSingleNationalIdentityNumber_ReturnsBadRequest()
- {
- // Arrange
- var invalidRequest = new UserContactPointLookup { NationalIdentityNumbers = { "invalid_format" } };
-
- // Act
- var response = await _controller.PostLookup(invalidRequest);
-
- // Assert
- var notFoundResult = Assert.IsType(response.Result);
- Assert.Equal(StatusCodes.Status404NotFound, notFoundResult.StatusCode);
- }
-
- [Fact]
- public async Task PostLookup_WithMixedNationalIdentityNumbers_ReturnsMixedResults()
- {
- // Arrange
- var request = new UserContactPointLookup { NationalIdentityNumbers = { "10060339738", "16051327393" } };
- var contactDetails = new ContactDetails
- {
- LanguageCode = "nb",
- Reservation = false,
- MobilePhoneNumber = "12345678",
- EmailAddress = "test@example.com",
- NationalIdentityNumber = "10060339738"
- };
- var lookupResult = new ContactDetailsLookupResult(matchedContactDetails: [contactDetails], unmatchedNationalIdentityNumbers: ["16051327393"]);
-
- _mockContactDetailsRetriever.Setup(x => x.RetrieveAsync(request)).ReturnsAsync(lookupResult);
-
- // Act
- var response = await _controller.PostLookup(request);
-
- // Assert
- var result = Assert.IsType(response.Result);
- Assert.Equal(StatusCodes.Status200OK, result.StatusCode);
- var returnValue = Assert.IsType(result.Value);
- Assert.Equal(lookupResult, returnValue);
- Assert.Single(returnValue.MatchedContactDetails);
- Assert.Single(returnValue.UnmatchedNationalIdentityNumbers);
- }
-
- [Fact]
- public async Task PostLookup_WithNoNationalIdentityNumbers_ReturnsBadRequest()
- {
- // Arrange
- var invalidRequest = new UserContactPointLookup { NationalIdentityNumbers = { } };
-
- // Act
- var response = await _controller.PostLookup(invalidRequest);
-
- // Assert
- var badRequestResult = Assert.IsType(response.Result);
- Assert.Equal(StatusCodes.Status400BadRequest, badRequestResult.StatusCode);
- Assert.Equal("National identity numbers cannot be null or empty.", badRequestResult.Value);
- }
-
- [Fact]
- public async Task PostLookup_WithValidRequest_ReturnsOk()
- {
- // Arrange
- var request = new UserContactPointLookup { NationalIdentityNumbers = { "27038893837" } };
- var contactDetails = new ContactDetails
- {
- LanguageCode = "nb",
- Reservation = false,
- MobilePhoneNumber = "12345678",
- EmailAddress = "test@example.com",
- NationalIdentityNumber = "27038893837"
- };
- var lookupResult = new ContactDetailsLookupResult(matchedContactDetails: [contactDetails], unmatchedNationalIdentityNumbers: []);
-
- _mockContactDetailsRetriever.Setup(x => x.RetrieveAsync(request)).ReturnsAsync(lookupResult);
-
- // Act
- var response = await _controller.PostLookup(request);
-
- // Assert
- var result = Assert.IsType(response.Result);
- Assert.Equal(StatusCodes.Status200OK, result.StatusCode);
- var returnValue = Assert.IsType(result.Value);
- Assert.Equal(lookupResult, returnValue);
- }
-}
diff --git a/test/Altinn.Profile.Tests/IntegrationTests/API/Controllers/ContactDetailsInternalControllerTests.cs b/test/Altinn.Profile.Tests/IntegrationTests/API/Controllers/ContactDetailsInternalControllerTests.cs
deleted file mode 100644
index b4490f8..0000000
--- a/test/Altinn.Profile.Tests/IntegrationTests/API/Controllers/ContactDetailsInternalControllerTests.cs
+++ /dev/null
@@ -1,169 +0,0 @@
-using System;
-using System.Linq;
-using System.Threading.Tasks;
-
-using Altinn.Profile.Controllers;
-using Altinn.Profile.Models;
-using Altinn.Profile.UseCases;
-
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Logging;
-
-using Moq;
-
-using Xunit;
-
-namespace Altinn.Profile.Tests.IntegrationTests.API.Controllers;
-
-public class ContactDetailsInternalControllerTests
-{
- private readonly ContactDetailsInternalController _controller;
- private readonly Mock> _loggerMock;
- private readonly Mock _mockContactDetailsRetriever;
-
- public ContactDetailsInternalControllerTests()
- {
- _loggerMock = new Mock>();
- _mockContactDetailsRetriever = new Mock();
- _controller = new ContactDetailsInternalController(_loggerMock.Object, _mockContactDetailsRetriever.Object);
- }
-
- [Fact]
- public void Constructor_WithNullContactDetailsRetriever_ThrowsArgumentNullException()
- {
- Assert.Throws(() => new ContactDetailsInternalController(_loggerMock.Object, null));
- }
-
- [Fact]
- public void Constructor_WithNullLogger_ThrowsArgumentNullException()
- {
- var contactDetailsRetrieverMock = new Mock();
- Assert.Throws(() => new ContactDetailsInternalController(null, contactDetailsRetrieverMock.Object));
- }
-
- [Fact]
- public void Constructor_WithValidParameters_InitializesCorrectly()
- {
- var contactDetailsRetrieverMock = new Mock();
- var controller = new ContactDetailsInternalController(_loggerMock.Object, contactDetailsRetrieverMock.Object);
- Assert.NotNull(controller);
- }
-
- [Fact]
- public async Task PostLookup_WhenRetrievalThrowsException_LogsErrorAndReturnsProblemResult()
- {
- var request = new UserContactPointLookup
- {
- NationalIdentityNumbers = ["05025308508"]
- };
-
- _mockContactDetailsRetriever.Setup(x => x.RetrieveAsync(request))
- .ThrowsAsync(new Exception("Some error occurred"));
-
- var response = await _controller.PostLookup(request);
-
- _loggerMock.Verify(
- x => x.Log(
- LogLevel.Error,
- It.IsAny(),
- It.Is((v, t) => v.ToString().Contains("An error occurred while retrieving contact details.")),
- It.IsAny(),
- It.IsAny>()),
- Times.Once);
-
- var problemResult = Assert.IsType(response.Result);
- Assert.Equal(StatusCodes.Status500InternalServerError, problemResult.StatusCode);
- Assert.Equal("An unexpected error occurred.", Convert.ToString(((ProblemDetails)problemResult.Value).Detail));
- }
-
- [Fact]
- public async Task PostLookup_WithEmptyNationalIdentityNumbers_ReturnsBadRequest()
- {
- var request = new UserContactPointLookup { NationalIdentityNumbers = [] };
- var response = await _controller.PostLookup(request);
- AssertBadRequest(response, "National identity numbers cannot be null or empty.");
- }
-
- [Fact]
- public async Task PostLookup_WithInvalidModelState_ReturnsBadRequest()
- {
- var request = new UserContactPointLookup
- {
- NationalIdentityNumbers = ["17092037169"]
- };
- _controller.ModelState.AddModelError("TestError", "Invalid data model");
-
- var response = await _controller.PostLookup(request);
- AssertBadRequest(response);
- }
-
- [Fact]
- public async Task PostLookup_WithMixedNationalIdentityNumbers_ReturnsMixedResults()
- {
- var request = new UserContactPointLookup
- {
- NationalIdentityNumbers = ["05025308508", "08110270527"]
- };
-
- var contactDetails = new ContactDetails
- {
- LanguageCode = "nb",
- Reservation = false,
- MobilePhoneNumber = "12345678",
- EmailAddress = "test@example.com",
- NationalIdentityNumber = "05025308508"
- };
-
- var lookupResult = new ContactDetailsLookupResult(
- matchedContactDetails: [contactDetails],
- unmatchedNationalIdentityNumbers: ["08110270527"]);
-
- _mockContactDetailsRetriever.Setup(x => x.RetrieveAsync(request))
- .ReturnsAsync(lookupResult);
-
- var response = await _controller.PostLookup(request);
-
- var result = Assert.IsType(response.Result);
- Assert.Equal(StatusCodes.Status200OK, result.StatusCode);
-
- var returnValue = Assert.IsType(result.Value);
- AssertContactDetailsLookupResult(lookupResult, returnValue);
- }
-
- [Fact]
- public async Task PostLookup_WithNullNationalIdentityNumbers_ReturnsBadRequest()
- {
- var request = new UserContactPointLookup { NationalIdentityNumbers = null };
- var response = await _controller.PostLookup(request);
- AssertBadRequest(response, "National identity numbers cannot be null or empty.");
- }
-
- private static void AssertBadRequest(ActionResult response, string expectedMessage = null)
- {
- var badRequestResult = Assert.IsType(response.Result);
- Assert.Equal(StatusCodes.Status400BadRequest, badRequestResult.StatusCode);
- if (expectedMessage != null)
- {
- Assert.Equal(expectedMessage, badRequestResult.Value);
- }
- }
-
- private static void AssertContactDetailsLookupResult(ContactDetailsLookupResult expected, ContactDetailsLookupResult actual)
- {
- Assert.Equal(expected, actual);
- Assert.Single(actual.MatchedContactDetails);
- Assert.Single(actual.UnmatchedNationalIdentityNumbers);
-
- var matchedContactDetails = actual.MatchedContactDetails.FirstOrDefault();
- Assert.NotNull(matchedContactDetails);
- Assert.Equal(expected.MatchedContactDetails.First().Reservation, matchedContactDetails.Reservation);
- Assert.Equal(expected.MatchedContactDetails.First().EmailAddress, matchedContactDetails.EmailAddress);
- Assert.Equal(expected.MatchedContactDetails.First().LanguageCode, matchedContactDetails.LanguageCode);
- Assert.Equal(expected.MatchedContactDetails.First().MobilePhoneNumber, matchedContactDetails.MobilePhoneNumber);
- Assert.Equal(expected.MatchedContactDetails.First().NationalIdentityNumber, matchedContactDetails.NationalIdentityNumber);
-
- var unmatchedNationalIdentityNumber = actual.UnmatchedNationalIdentityNumbers.FirstOrDefault();
- Assert.Equal(expected.UnmatchedNationalIdentityNumbers.First(), unmatchedNationalIdentityNumber);
- }
-}
diff --git a/test/Altinn.Profile.Tests/IntegrationTests/API/Controllers/PersonContactDetailsControllerTests.cs b/test/Altinn.Profile.Tests/IntegrationTests/API/Controllers/PersonContactDetailsControllerTests.cs
new file mode 100644
index 0000000..853a218
--- /dev/null
+++ b/test/Altinn.Profile.Tests/IntegrationTests/API/Controllers/PersonContactDetailsControllerTests.cs
@@ -0,0 +1,407 @@
+using System;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Net.Http.Json;
+using System.Text.Json;
+using System.Threading.Tasks;
+
+using Altinn.Profile.Controllers;
+using Altinn.Profile.Models;
+using Altinn.Profile.Tests.IntegrationTests.Utils;
+using Altinn.Profile.UseCases;
+
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Testing;
+using Microsoft.Extensions.Logging;
+
+using Moq;
+
+using Xunit;
+
+namespace Altinn.Profile.Tests.IntegrationTests.API.Controllers;
+
+public class PersonContactDetailsControllerTests
+{
+ private readonly JsonSerializerOptions _serializerOptions;
+ private readonly PersonContactDetailsController _controller;
+ private readonly Mock> _loggerMock;
+ private readonly Mock _mockContactDetailsRetriever;
+ private readonly WebApplicationFactorySetup _webApplicationFactorySetup;
+
+ public PersonContactDetailsControllerTests()
+ {
+ _loggerMock = new Mock>();
+ _mockContactDetailsRetriever = new Mock();
+ _serializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
+ _controller = new PersonContactDetailsController(_loggerMock.Object, _mockContactDetailsRetriever.Object);
+ _webApplicationFactorySetup = new WebApplicationFactorySetup(new WebApplicationFactory());
+ }
+
+ [Fact]
+ public void Constructor_WithValidParameters_InitializesCorrectly()
+ {
+ // Arrange
+ var loggerMock = new Mock>();
+ var contactDetailsRetrieverMock = new Mock();
+
+ // Act
+ var controller = new PersonContactDetailsController(loggerMock.Object, contactDetailsRetrieverMock.Object);
+
+ // Assert
+ Assert.NotNull(controller);
+ }
+
+ [Fact]
+ public async Task PostLookup_WhenExceptionOccurs_ReturnsInternalServerError_And_LogsError()
+ {
+ // Arrange
+ var request = new UserContactDetailsLookupCriteria { NationalIdentityNumbers = { "27038893837" } };
+ _mockContactDetailsRetriever.Setup(x => x.RetrieveAsync(request)).ThrowsAsync(new Exception("Test exception"));
+
+ // Act
+ var response = await _controller.PostLookup(request);
+
+ // Assert
+ var problemResult = Assert.IsType(response.Result);
+ Assert.Equal(StatusCodes.Status500InternalServerError, problemResult.StatusCode);
+
+ _loggerMock.Verify(
+ x => x.Log(
+ LogLevel.Error,
+ It.IsAny(),
+ It.Is((v, t) => v.ToString().Contains("An error occurred while retrieving contact details.")),
+ It.IsAny(),
+ It.IsAny>()),
+ Times.Once);
+ }
+
+ [Fact]
+ public async Task PostLookup_WhenLookupCriteriaIsNull_ReturnsBadRequest()
+ {
+ // Arrange
+ UserContactDetailsLookupCriteria request = null;
+
+ // Act
+ var response = await _controller.PostLookup(request);
+
+ // Assert
+ var badRequestResult = Assert.IsType(response.Result);
+ Assert.Equal(StatusCodes.Status400BadRequest, badRequestResult.StatusCode);
+ Assert.Equal("National identity numbers cannot be null or empty.", badRequestResult.Value);
+ }
+
+ [Fact]
+ public async Task PostLookup_WhenMatchedContactDetailsAreFound_ReturnsOk()
+ {
+ // Arrange
+ var request = new UserContactDetailsLookupCriteria { NationalIdentityNumbers = { "05053423096" } };
+ var contactDetails = new PersonContactDetails
+ {
+ LanguageCode = "en",
+ IsReserved = false,
+ MobilePhoneNumber = "98765432",
+ EmailAddress = "user@example.com",
+ NationalIdentityNumber = "05053423096"
+ };
+ var lookupResult = new PersonContactDetailsLookupResult(matchedPersonContactDetails: [contactDetails], unmatchedNationalIdentityNumbers: []);
+
+ _mockContactDetailsRetriever.Setup(x => x.RetrieveAsync(request)).ReturnsAsync(lookupResult);
+
+ // Act
+ var response = await _controller.PostLookup(request);
+
+ // Assert
+ var result = Assert.IsType(response.Result);
+ Assert.Equal(StatusCodes.Status200OK, result.StatusCode);
+
+ var returnValue = Assert.IsType(result.Value);
+ Assert.NotNull(returnValue);
+ Assert.Single(returnValue.MatchedPersonContactDetails);
+ Assert.Empty(returnValue.UnmatchedNationalIdentityNumbers);
+ }
+
+ [Fact]
+ public async Task PostLookup_WhenNationalIdentityNumbersIsNull_ReturnsBadRequest()
+ {
+ // Arrange
+ var request = new UserContactDetailsLookupCriteria { NationalIdentityNumbers = null };
+
+ // Act
+ var response = await _controller.PostLookup(request);
+
+ // Assert
+ var badRequestResult = Assert.IsType(response.Result);
+ Assert.Equal(StatusCodes.Status400BadRequest, badRequestResult.StatusCode);
+ Assert.Equal("National identity numbers cannot be null or empty.", badRequestResult.Value);
+ }
+
+ [Fact]
+ public async Task PostLookup_WhenNoContactDetailsFound_ReturnsNotFound()
+ {
+ // Arrange
+ var request = new UserContactDetailsLookupCriteria { NationalIdentityNumbers = { "30083542175" } };
+ var lookupResult = new PersonContactDetailsLookupResult(matchedPersonContactDetails: [], unmatchedNationalIdentityNumbers: ["30083542175"]);
+
+ _mockContactDetailsRetriever.Setup(x => x.RetrieveAsync(request)).ReturnsAsync(lookupResult);
+
+ // Act
+ var response = await _controller.PostLookup(request);
+
+ // Assert
+ var notFoundResult = Assert.IsType(response.Result);
+ Assert.Equal(StatusCodes.Status404NotFound, notFoundResult.StatusCode);
+ }
+
+ [Fact]
+ public async Task PostLookup_WhenNoMatchedContactDetailsAreFound_ReturnsNotFound()
+ {
+ // Arrange
+ var request = new UserContactDetailsLookupCriteria { NationalIdentityNumbers = { "99020312345" } };
+ var lookupResult = new PersonContactDetailsLookupResult(matchedPersonContactDetails: [], unmatchedNationalIdentityNumbers: ["99020312345"]);
+
+ _mockContactDetailsRetriever.Setup(x => x.RetrieveAsync(request)).ReturnsAsync(lookupResult);
+
+ // Act
+ var response = await _controller.PostLookup(request);
+
+ // Assert
+ var notFoundResult = Assert.IsType(response.Result);
+ Assert.Equal(StatusCodes.Status404NotFound, notFoundResult.StatusCode);
+ }
+
+ [Fact]
+ public async Task PostLookup_WhenServiceCallIsLongRunning_DoesNotBlock()
+ {
+ // Arrange
+ var request = new UserContactDetailsLookupCriteria { NationalIdentityNumbers = { "02038112735" } };
+ var contactDetails = new PersonContactDetails
+ {
+ LanguageCode = "nb",
+ IsReserved = false,
+ MobilePhoneNumber = "12345678",
+ EmailAddress = "test@example.com",
+ NationalIdentityNumber = "02038112735"
+ };
+ var lookupResult = new PersonContactDetailsLookupResult(matchedPersonContactDetails: [contactDetails], unmatchedNationalIdentityNumbers: []);
+
+ _mockContactDetailsRetriever.Setup(x => x.RetrieveAsync(request)).ReturnsAsync(() =>
+ {
+ Task.Delay(5000).Wait(); // Simulate long-running task
+ return lookupResult;
+ });
+
+ // Act
+ var response = await _controller.PostLookup(request);
+
+ // Assert
+ var result = Assert.IsType(response.Result);
+ Assert.Equal(StatusCodes.Status200OK, result.StatusCode);
+ Assert.Equal(lookupResult, Assert.IsType(result.Value));
+ }
+
+ [Fact]
+ public async Task PostLookup_WithInvalidModelState_ReturnsBadRequest()
+ {
+ // Arrange
+ var invalidRequest = new UserContactDetailsLookupCriteria { NationalIdentityNumbers = { "14078112078" } };
+ _controller.ModelState.AddModelError("InvalidKey", "Invalid error message");
+
+ // Act
+ var response = await _controller.PostLookup(invalidRequest);
+
+ // Assert
+ var badRequestResult = Assert.IsType(response.Result);
+ Assert.Equal(StatusCodes.Status400BadRequest, badRequestResult.StatusCode);
+ }
+
+ [Fact]
+ public async Task PostLookup_WithInvalidSingleNationalIdentityNumber_ReturnsBadRequest()
+ {
+ // Arrange
+ var invalidRequest = new UserContactDetailsLookupCriteria { NationalIdentityNumbers = { "invalid_format" } };
+
+ // Act
+ var response = await _controller.PostLookup(invalidRequest);
+
+ // Assert
+ var notFoundResult = Assert.IsType(response.Result);
+ Assert.Equal(StatusCodes.Status404NotFound, notFoundResult.StatusCode);
+ }
+
+ [Fact]
+ public async Task PostLookup_WithMixedNationalIdentityNumbers_ReturnsMixedResults()
+ {
+ // Arrange
+ var request = new UserContactDetailsLookupCriteria { NationalIdentityNumbers = { "10060339738", "16051327393" } };
+ var contactDetails = new PersonContactDetails
+ {
+ LanguageCode = "nb",
+ IsReserved = false,
+ MobilePhoneNumber = "12345678",
+ EmailAddress = "test@example.com",
+ NationalIdentityNumber = "10060339738"
+ };
+ var lookupResult = new PersonContactDetailsLookupResult(matchedPersonContactDetails: [contactDetails], unmatchedNationalIdentityNumbers: ["16051327393"]);
+
+ _mockContactDetailsRetriever.Setup(x => x.RetrieveAsync(request)).ReturnsAsync(lookupResult);
+
+ // Act
+ var response = await _controller.PostLookup(request);
+
+ // Assert
+ var result = Assert.IsType(response.Result);
+ Assert.Equal(StatusCodes.Status200OK, result.StatusCode);
+ var returnValue = Assert.IsType(result.Value);
+ Assert.Equal(lookupResult, returnValue);
+ Assert.Single(returnValue.MatchedPersonContactDetails);
+ Assert.Single(returnValue.UnmatchedNationalIdentityNumbers);
+ }
+
+ [Fact]
+ public async Task PostLookup_WithNoNationalIdentityNumbers_ReturnsBadRequest()
+ {
+ // Arrange
+ var invalidRequest = new UserContactDetailsLookupCriteria { NationalIdentityNumbers = { } };
+
+ // Act
+ var response = await _controller.PostLookup(invalidRequest);
+
+ // Assert
+ var badRequestResult = Assert.IsType(response.Result);
+ Assert.Equal(StatusCodes.Status400BadRequest, badRequestResult.StatusCode);
+ Assert.Equal("National identity numbers cannot be null or empty.", badRequestResult.Value);
+ }
+
+ [Fact]
+ public async Task PostLookup_WithValidRequest_ReturnsOk()
+ {
+ // Arrange
+ var request = new UserContactDetailsLookupCriteria { NationalIdentityNumbers = { "27038893837" } };
+ var contactDetails = new PersonContactDetails
+ {
+ LanguageCode = "nb",
+ IsReserved = false,
+ MobilePhoneNumber = "12345678",
+ EmailAddress = "test@example.com",
+ NationalIdentityNumber = "27038893837"
+ };
+ var lookupResult = new PersonContactDetailsLookupResult(matchedPersonContactDetails: [contactDetails], unmatchedNationalIdentityNumbers: []);
+
+ _mockContactDetailsRetriever.Setup(x => x.RetrieveAsync(request)).ReturnsAsync(lookupResult);
+
+ // Act
+ var response = await _controller.PostLookup(request);
+
+ // Assert
+ var result = Assert.IsType(response.Result);
+ Assert.Equal(StatusCodes.Status200OK, result.StatusCode);
+ var returnValue = Assert.IsType(result.Value);
+ Assert.Equal(lookupResult, returnValue);
+ }
+
+ ////[Fact]
+ ////public async Task PostLookup_WithValidNationalIdentityNumbers_ReturnsValidResults_IntegrationTest()
+ ////{
+ //// // Arrange
+ //// HttpClient client = _webApplicationFactorySetup.GetTestServerClient();
+ //// var lookupCriteria = new PersonContactDetailsLookupCriteria
+ //// {
+ //// NationalIdentityNumbers = ["02018090573", "03070100664", "03074500217"]
+ //// };
+
+ //// HttpRequestMessage httpRequestMessage = CreatePostRequest("/profile/api/v1/person/contact/details/lookup");
+ //// httpRequestMessage.Content = JsonContent.Create(lookupCriteria);
+
+ //// // Act
+ //// HttpResponseMessage response = await client.SendAsync(httpRequestMessage);
+
+ //// // Assert
+ //// Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+
+ //// string responseContent = await response.Content.ReadAsStringAsync();
+ //// PersonContactDetailsLookupResult lookupResult = JsonSerializer.Deserialize(responseContent, _serializerOptions);
+
+ //// Assert.NotNull(lookupResult);
+ //// Assert.NotNull(lookupResult.MatchedPersonContactDetails);
+ //// Assert.Null(lookupResult.UnmatchedNationalIdentityNumbers);
+ //// Assert.Equal(3, lookupResult.MatchedPersonContactDetails.Count);
+ ////}
+
+ [Fact]
+ public async Task PostLookup_WhenNationalIdentityNumbersIsNull_ReturnsBadRequest_IntegrationTest()
+ {
+ // Arrange
+ var client = _webApplicationFactorySetup.GetTestServerClient();
+ var lookupCriteria = new UserContactDetailsLookupCriteria
+ {
+ NationalIdentityNumbers = null
+ };
+ HttpRequestMessage httpRequestMessage = CreatePostRequest("/profile/api/v1/person/contact/details/lookup");
+ httpRequestMessage.Content = JsonContent.Create(lookupCriteria);
+
+ // Act
+ HttpResponseMessage response = await client.SendAsync(httpRequestMessage);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task PostLookup_WhenNoContactDetailsFound_ReturnsNotFound_IntegrationTest()
+ {
+ // Arrange
+ var client = _webApplicationFactorySetup.GetTestServerClient();
+ var lookupCriteria = new UserContactDetailsLookupCriteria
+ {
+ NationalIdentityNumbers = ["false"]
+ };
+ HttpRequestMessage httpRequestMessage = CreatePostRequest("/profile/api/v1/person/contact/details/lookup");
+ httpRequestMessage.Content = JsonContent.Create(lookupCriteria);
+
+ // Act
+ HttpResponseMessage response = await client.SendAsync(httpRequestMessage);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ }
+
+ ////[Fact]
+ ////public async Task PostLookup_WithMixedNationalIdentityNumbers_ReturnsMixedResults_IntegrationTest()
+ ////{
+ //// // Arrange
+ //// HttpClient client = _webApplicationFactorySetup.GetTestServerClient();
+ //// var lookupCriteria = new PersonContactDetailsLookupCriteria
+ //// {
+ //// NationalIdentityNumbers = ["07875499461", "none", "07844998311"]
+ //// };
+
+ //// HttpRequestMessage httpRequestMessage = CreatePostRequest("/profile/api/v1/person/contact/details/lookup");
+ //// httpRequestMessage.Content = JsonContent.Create(lookupCriteria);
+
+ //// // Act
+ //// HttpResponseMessage response = await client.SendAsync(httpRequestMessage);
+
+ //// // Assert
+ //// Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+
+ //// string responseContent = await response.Content.ReadAsStringAsync();
+ //// PersonContactDetailsLookupResult lookupResult = JsonSerializer.Deserialize(responseContent, _serializerOptions);
+
+ //// Assert.NotNull(lookupResult);
+ //// Assert.NotNull(lookupResult.MatchedPersonContactDetails);
+ //// Assert.Single(lookupResult.UnmatchedNationalIdentityNumbers);
+ //// Assert.NotNull(lookupResult.UnmatchedNationalIdentityNumbers);
+ //// Assert.Equal(2, lookupResult.MatchedPersonContactDetails.Count);
+ ////}
+
+ private static HttpRequestMessage CreatePostRequest(string requestUri)
+ {
+ int userId = 2516356;
+ HttpRequestMessage httpRequestMessage = new(HttpMethod.Post, requestUri);
+ string token = PrincipalUtil.GetToken(userId);
+ httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
+ return httpRequestMessage;
+ }
+}
diff --git a/test/Altinn.Profile.Tests/IntegrationTests/API/Controllers/PersonContactDetailsInternalControllerTests.cs b/test/Altinn.Profile.Tests/IntegrationTests/API/Controllers/PersonContactDetailsInternalControllerTests.cs
new file mode 100644
index 0000000..1f175d0
--- /dev/null
+++ b/test/Altinn.Profile.Tests/IntegrationTests/API/Controllers/PersonContactDetailsInternalControllerTests.cs
@@ -0,0 +1,268 @@
+using System;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Net.Http.Json;
+using System.Text.Json;
+using System.Threading.Tasks;
+
+using Altinn.Profile.Controllers;
+using Altinn.Profile.Models;
+using Altinn.Profile.Tests.IntegrationTests.Utils;
+using Altinn.Profile.UseCases;
+
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Testing;
+using Microsoft.Extensions.Logging;
+
+using Moq;
+
+using Xunit;
+
+namespace Altinn.Profile.Tests.IntegrationTests.API.Controllers;
+
+public class PersonContactDetailsInternalControllerTests
+{
+ private readonly JsonSerializerOptions _serializerOptions;
+ private readonly PersonContactDetailsInternalController _controller;
+ private readonly Mock> _loggerMock;
+ private readonly Mock _mockContactDetailsRetriever;
+ private readonly WebApplicationFactorySetup _webApplicationFactorySetup;
+
+ public PersonContactDetailsInternalControllerTests()
+ {
+ _loggerMock = new Mock>();
+ _mockContactDetailsRetriever = new Mock();
+ _serializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
+ _controller = new PersonContactDetailsInternalController(_loggerMock.Object, _mockContactDetailsRetriever.Object);
+ _webApplicationFactorySetup = new WebApplicationFactorySetup(new WebApplicationFactory());
+ }
+
+ [Fact]
+ public void Constructor_WithValidParameters_InitializesCorrectly()
+ {
+ var contactDetailsRetrieverMock = new Mock();
+ var controller = new PersonContactDetailsInternalController(_loggerMock.Object, contactDetailsRetrieverMock.Object);
+ Assert.NotNull(controller);
+ }
+
+ [Fact]
+ public async Task PostLookup_WhenRetrievalThrowsException_LogsErrorAndReturnsProblemResult()
+ {
+ var request = new UserContactDetailsLookupCriteria
+ {
+ NationalIdentityNumbers = ["05025308508"]
+ };
+
+ _mockContactDetailsRetriever.Setup(x => x.RetrieveAsync(request))
+ .ThrowsAsync(new Exception("Some error occurred"));
+
+ var response = await _controller.PostLookup(request);
+
+ _loggerMock.Verify(
+ x => x.Log(
+ LogLevel.Error,
+ It.IsAny(),
+ It.Is