Skip to content
Open
13 changes: 10 additions & 3 deletions src/Altinn.App.Clients.Fiks/Constants/FiksArkivConstants.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using Altinn.App.Clients.Fiks.FiksArkiv.Models;
using KS.Fiks.Arkiv.Models.V1.Kodelister;
using KS.Fiks.Arkiv.Models.V1.Meldingstyper;

namespace Altinn.App.Clients.Fiks.Constants;
Expand Down Expand Up @@ -37,11 +39,16 @@ public static class MessageTypes
public const string ArchiveRecordCreationReceipt = FiksArkivMeldingtype.ArkivmeldingOpprettKvittering;
}

/// <summary>
/// Classification IDs for Fiks Arkiv messages.
/// </summary>
/// <remarks>Callers can also specify their own, additional, classifications via
/// <see cref="FiksArkivMetadataSettings.CaseFileClassifications"/>.</remarks>
internal static class ClassificationId
{
public const string NationalIdentityNumber = "Fødselsnummer";
public const string OrganizationNumber = "Organisasjonsnummer";
public static readonly string NationalIdentityNumber = KlassifikasjonstypeKoder.Foedselsnummer.Verdi;
public const string OrganizationNumber = "ORGNR";
public const string AltinnUserId = "AltinnBrukerId";
public const string SystemUserId = "SystembrukerId";
public const string SystemUserId = "AltinnSystembrukerId";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Altinn.App.Clients.Fiks.FiksArkiv.Models;
using KS.Fiks.Arkiv.Models.V1.Arkivering.Arkivmelding;

namespace Altinn.App.Clients.Fiks.Extensions;

internal static class FiksArkivClassificationExtensions
{
public static Klassifikasjon ToKlassifikasjon(this FiksArkivClassification classification) =>
new()
{
KlassifikasjonssystemID = classification.SystemId,
KlasseID = classification.ClassificationId,
Tittel = classification.Title,
ErSkjermet = classification.IsRestricted,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@
KlassifikasjonssystemID = FiksArkivConstants.ClassificationId.NationalIdentityNumber,
KlasseID = userProfile
.Party.SSN.ToString(CultureInfo.InvariantCulture)
.EnsureNotNullOrEmpty("Classification.Id"),

Check warning on line 26 in src/Altinn.App.Clients.Fiks/Factories/KlassifikasjonFactory.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of using this literal 'Classification.Id' 5 times.

See more on https://sonarcloud.io/project/issues?id=Altinn_app-lib-dotnet&issues=AZ5VwKnNUEPGbqIct6V8&open=AZ5VwKnNUEPGbqIct6V8&pullRequest=1765
Tittel = userProfile.Party.Name.EnsureNotEmpty("Classification.Title"),

Check warning on line 27 in src/Altinn.App.Clients.Fiks/Factories/KlassifikasjonFactory.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of using this literal 'Classification.Title' 4 times.

See more on https://sonarcloud.io/project/issues?id=Altinn_app-lib-dotnet&issues=AZ5VwKnNUEPGbqIct6V9&open=AZ5VwKnNUEPGbqIct6V9&pullRequest=1765
ErSkjermet = true,
}
: new Klassifikasjon
{
Expand Down
38 changes: 25 additions & 13 deletions src/Altinn.App.Clients.Fiks/FiksArkiv/FiksArkivConfigResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,16 @@
private readonly GeneralSettings _generalSettings;
private readonly IAltinnPartyClient _altinnPartyClient;

public FiksArkivConfigResolver(
IOptions<FiksArkivSettings> fiksArkivSettings,
IAppMetadata appMetadata,
ITranslationService translationService,
InstanceDataUnitOfWorkInitializer instanceDataUnitOfWorkInitializer,
ILayoutEvaluatorStateInitializer layoutStateInitializer,
IOptions<GeneralSettings> generalSettings,
IAltinnPartyClient altinnPartyClient,
ILogger<FiksArkivConfigResolver> logger
)

Check warning on line 42 in src/Altinn.App.Clients.Fiks/FiksArkiv/FiksArkivConfigResolver.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Constructor has 8 parameters, which is greater than the 7 authorized.

See more on https://sonarcloud.io/project/issues?id=Altinn_app-lib-dotnet&issues=AZ5ProF4pn65xSQ3zX69&open=AZ5ProF4pn65xSQ3zX69&pullRequest=1765
{
_fiksArkivSettings = fiksArkivSettings.Value;
_appMetadata = appMetadata;
Expand Down Expand Up @@ -121,7 +121,7 @@

return new FiksArkivDocumentMetadata(systemId, ruleId, caseFileId, caseFileTitle, journalEntryTitle);
}
catch (Exception e)

Check warning on line 124 in src/Altinn.App.Clients.Fiks/FiksArkiv/FiksArkivConfigResolver.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Either log this exception and handle it, or rethrow it with some contextual information.

See more on https://sonarcloud.io/project/issues?id=Altinn_app-lib-dotnet&issues=AZ5ProF4pn65xSQ3zX6-&open=AZ5ProF4pn65xSQ3zX6-&pullRequest=1765
{
_logger.LogError(e, "Fiks Arkiv error: {Error}", e.Message);
throw;
Expand Down Expand Up @@ -184,7 +184,7 @@

return new FiksArkivRecipient(accountId, identifier, name, orgNumber);
}
catch (Exception e)

Check warning on line 187 in src/Altinn.App.Clients.Fiks/FiksArkiv/FiksArkivConfigResolver.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Either log this exception and handle it, or rethrow it with some contextual information.

See more on https://sonarcloud.io/project/issues?id=Altinn_app-lib-dotnet&issues=AZ5ProF4pn65xSQ3zX6_&open=AZ5ProF4pn65xSQ3zX6_&pullRequest=1765
{
_logger.LogError(e, "Fiks Arkiv error: {Error}", e.Message);
throw;
Expand Down Expand Up @@ -212,23 +212,16 @@
);

/// <inheritdoc />
public async Task<Klassifikasjon> GetInstanceOwnerClassification(
public async Task<IReadOnlyList<Klassifikasjon>> GetCaseFileClassifications(
Authenticated auth,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();

return auth switch
{
Authenticated.User user => await KlassifikasjonFactory.CreateUser(user), // Note: Doesn't accept cancellation token.. yet
Authenticated.SystemUser systemUser => KlassifikasjonFactory.CreateSystemUser(systemUser),
Authenticated.ServiceOwner serviceOwner => KlassifikasjonFactory.CreateServiceOwner(serviceOwner),
Authenticated.Org org => KlassifikasjonFactory.CreateOrganization(org),
_ => throw new FiksArkivException(
$"Could not determine submitter details from authentication context: {auth}"
),
};
return
[
await GetInstanceOwnerClassification(auth, cancellationToken: cancellationToken),
.. _fiksArkivSettings.Metadata?.CaseFileClassifications?.Select(x => x.ToKlassifikasjon()) ?? [],
];
}

/// <inheritdoc />
Expand Down Expand Up @@ -300,6 +293,25 @@
return await _layoutStateInitializer.Init(unitOfWork, null);
}

private static async Task<Klassifikasjon> GetInstanceOwnerClassification(
Authenticated auth,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();

return auth switch
{
Authenticated.User user => await KlassifikasjonFactory.CreateUser(user), // Note: Doesn't accept cancellation token.. yet
Authenticated.SystemUser systemUser => KlassifikasjonFactory.CreateSystemUser(systemUser),
Authenticated.ServiceOwner serviceOwner => KlassifikasjonFactory.CreateServiceOwner(serviceOwner),
Authenticated.Org org => KlassifikasjonFactory.CreateOrganization(org),
_ => throw new FiksArkivException(
$"Could not determine submitter details from authentication context: {auth}"
),
};
}

private static async Task<T?> GetBindableConfigValue<T>(
LayoutEvaluatorState layoutState,
Instance instance,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
using KS.Fiks.Arkiv.Models.V1.Metadatakatalog;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Kode = KS.Fiks.Arkiv.Models.V1.Kodelister.Kode;

namespace Altinn.App.Clients.Fiks.FiksArkiv;
Expand All @@ -27,7 +26,6 @@
private readonly ILogger<FiksArkivDefaultPayloadGenerator> _logger;
private readonly IHostEnvironment _hostEnvironment;
private readonly IFiksArkivConfigResolver _fiksArkivConfigResolver;
private readonly FiksIOSettings _fiksIOSettings;
private readonly TimeProvider _timeProvider;

private bool _indentXmlSerialization => !_hostEnvironment.IsProduction();
Expand All @@ -39,7 +37,6 @@
ILogger<FiksArkivDefaultPayloadGenerator> logger,
IHostEnvironment hostEnvironment,
IFiksArkivConfigResolver fiksArkivConfigResolver,
IOptions<FiksIOSettings> fiksIOSettings,
TimeProvider? timeProvider = null
)
{
Expand All @@ -49,7 +46,6 @@
_logger = logger;
_hostEnvironment = hostEnvironment;
_fiksArkivConfigResolver = fiksArkivConfigResolver;
_fiksIOSettings = fiksIOSettings.Value;
_timeProvider = timeProvider ?? TimeProvider.System;
}

Expand All @@ -67,14 +63,15 @@
$"Unsupported message type: {messageType}. {nameof(FiksArkivDefaultPayloadGenerator)} can only handle {FiksArkivConstants.MessageTypes.CreateArchiveRecord} requests."
);

var now = _timeProvider.GetUtcNow();
var appMetadata = await _appMetadata.GetApplicationMetadata();
var documentCreator = appMetadata.AppIdentifier.Org;
var archiveDocuments = await GetArchiveDocuments(instance, cancellationToken);
var defaultDocumentTitle = await _fiksArkivConfigResolver.GetApplicationTitle(cancellationToken);
var documentMetadata = await _fiksArkivConfigResolver.GetArchiveDocumentMetadata(instance, cancellationToken);
var recipientParty = _fiksArkivConfigResolver.GetRecipientParty(instance, recipient);
var instanceOwnerParty = await _fiksArkivConfigResolver.GetInstanceOwnerParty(instance, cancellationToken);
var instanceOwnerClassification = await _fiksArkivConfigResolver.GetInstanceOwnerClassification(
var caseFileClassifications = await _fiksArkivConfigResolver.GetCaseFileClassifications(
_authenticationContext.Current,
cancellationToken
);
Expand All @@ -84,22 +81,25 @@
Tittel = documentMetadata?.CaseFileTitle ?? defaultDocumentTitle,
OffentligTittel = documentMetadata?.CaseFileTitle ?? defaultDocumentTitle,
AdministrativEnhet = new AdministrativEnhet { Navn = documentCreator },
Saksaar = _timeProvider.GetLocalNow().Year,
Saksdato = _timeProvider.GetLocalNow().DateTime,
Saksaar = now.Year,
Saksdato = now.UtcDateTime,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
ReferanseEksternNoekkel = new EksternNoekkel
{
Fagsystem = appMetadata.AppIdentifier.ToString(),
Noekkel = documentMetadata?.CaseFileId ?? instance.Id,
},
};

caseFile.Klassifikasjon.Add(instanceOwnerClassification);
foreach (var classification in caseFileClassifications)
{
caseFile.Klassifikasjon.Add(classification);
}

var journalEntry = new Journalpost
{
Journalaar = _timeProvider.GetLocalNow().Year,
DokumentetsDato = _timeProvider.GetLocalNow().DateTime,
SendtDato = _timeProvider.GetLocalNow().DateTime,
Journalaar = now.Year,
DokumentetsDato = now.UtcDateTime,
SendtDato = now.UtcDateTime,
Tittel = documentMetadata?.JournalEntryTitle ?? defaultDocumentTitle,
OffentligTittel = documentMetadata?.JournalEntryTitle ?? defaultDocumentTitle,
OpprettetAv = documentCreator,
Expand Down Expand Up @@ -131,12 +131,12 @@
}

// Main form data file
journalEntry.Dokumentbeskrivelse.Add(GetDocumentDescription(archiveDocuments.PrimaryDocument));
journalEntry.Dokumentbeskrivelse.Add(GetDocumentDescription(archiveDocuments.PrimaryDocument, now));

// Attachments
foreach (var attachment in archiveDocuments.AttachmentDocuments)
{
journalEntry.Dokumentbeskrivelse.Add(GetDocumentDescription(attachment));
journalEntry.Dokumentbeskrivelse.Add(GetDocumentDescription(attachment, now));
}

// Archive record
Expand Down Expand Up @@ -170,6 +170,7 @@
primaryDataElement,
primaryDocumentSettings.Filename,
DokumenttypeKoder.Dokument,
primaryDocumentSettings.FormatCode,
instanceId,
cancellationToken
);
Expand All @@ -181,7 +182,7 @@
.GetOptionalDataElements(attachmentSetting.DataType)
.ToList();

if (dataElements.Any() is false)

Check warning on line 185 in src/Altinn.App.Clients.Fiks/FiksArkiv/FiksArkivDefaultPayloadGenerator.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the unnecessary Boolean literal(s).

See more on https://sonarcloud.io/project/issues?id=Altinn_app-lib-dotnet&issues=AZ5ProFipn65xSQ3zX67&open=AZ5ProFipn65xSQ3zX67&pullRequest=1765
continue;

attachmentDocuments.AddRange(
Expand All @@ -191,6 +192,7 @@
x,
attachmentSetting.Filename,
DokumenttypeKoder.Vedlegg,
attachmentSetting.FormatCode,
instanceId,
cancellationToken
)
Expand All @@ -206,11 +208,12 @@
DataElement dataElement,
string? filename,
Kode fileTypeCode,
string? fileFormatCode,
InstanceIdentifier instanceId,
CancellationToken cancellationToken = default
)
{
if (string.IsNullOrWhiteSpace(filename) is false)

Check warning on line 216 in src/Altinn.App.Clients.Fiks/FiksArkiv/FiksArkivDefaultPayloadGenerator.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the unnecessary Boolean literal(s).

See more on https://sonarcloud.io/project/issues?id=Altinn_app-lib-dotnet&issues=AZ5ProFipn65xSQ3zX68&open=AZ5ProFipn65xSQ3zX68&pullRequest=1765
dataElement.Filename = filename;
else if (string.IsNullOrWhiteSpace(dataElement.Filename))
dataElement.Filename = $"{dataElement.DataType}{dataElement.GetExtensionForContentType()}";
Expand All @@ -225,11 +228,12 @@
cancellationToken: cancellationToken
)
),
fileTypeCode
fileTypeCode,
fileFormatCode
);
}

private Dokumentbeskrivelse GetDocumentDescription(MessagePayloadWrapper payloadWrapper)
private static Dokumentbeskrivelse GetDocumentDescription(MessagePayloadWrapper payloadWrapper, DateTimeOffset now)
{
var documentClassification =
payloadWrapper.FileTypeCode == DokumenttypeKoder.Dokument
Expand All @@ -254,24 +258,19 @@
KodeProperty = documentClassification.Verdi,
Beskrivelse = documentClassification.Beskrivelse,
},
OpprettetDato = _timeProvider.GetLocalNow().DateTime,
OpprettetDato = now.UtcDateTime,
};

metadata.Dokumentobjekt.Add(
new Dokumentobjekt
{
SystemID = new SystemID
{
Value = _fiksIOSettings.AccountId.ToString(),
Label = FiksArkivConstants.AltinnSystemId,
},
Filnavn = payloadWrapper.Payload.Filename,
ReferanseDokumentfil = payloadWrapper.Payload.Filename,
Format = new Format { KodeProperty = payloadWrapper.Payload.GetDotlessFileExtension() },
Format = payloadWrapper.GetFileFormat(),
Variantformat = new Variantformat
{
KodeProperty = VariantformatKoder.Produksjonsformat.Verdi,
Beskrivelse = VariantformatKoder.Produksjonsformat.Beskrivelse,
KodeProperty = VariantformatKoder.Arkivformat.Verdi,
Beskrivelse = VariantformatKoder.Arkivformat.Beskrivelse,
},
}
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,11 @@ public interface IFiksArkivConfigResolver
Task<Korrespondansepart?> GetInstanceOwnerParty(Instance instance, CancellationToken cancellationToken = default);

/// <summary>
/// Gets the classification of the instance owner (klassifikasjon).
/// Gets the case file classifications (klassifikasjoner) for the shipment.
/// Always includes the instance owner classification derived from <paramref name="auth"/>, followed by any
/// classifications configured in <see cref="FiksArkivMetadataSettings.CaseFileClassifications"/>.
/// </summary>
Task<Klassifikasjon> GetInstanceOwnerClassification(
Task<IReadOnlyList<Klassifikasjon>> GetCaseFileClassifications(
Authenticated auth,
CancellationToken cancellationToken = default
);
Expand Down
74 changes: 74 additions & 0 deletions src/Altinn.App.Clients.Fiks/FiksArkiv/Models/FiksArkivSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,13 @@
[JsonPropertyName("caseFileTitle")]
public FiksArkivBindableValue<string>? CaseFileTitle { get; set; }

/// <summary>
/// Optional classifications (klassifikasjon) to attach to the generated saksmappe (case file) element in the arkivmelding.xml.
/// These are appended in order after the implicit instance-owner classification.
/// </summary>
[JsonPropertyName("caseFileClassifications")]
public IReadOnlyList<FiksArkivClassification>? CaseFileClassifications { get; set; }

/// <summary>
/// The title to use for the generated journalpost (journal entry) element in the arkivmelding.xml.
/// If no title is provided, the value will default to the application title as defined in applicationmetadata.json.
Expand All @@ -141,6 +148,11 @@
CaseFileId?.Validate($"{propertyName}.{nameof(CaseFileId)}", dataTypes, appModelResolver);
CaseFileTitle?.Validate($"{propertyName}.{nameof(CaseFileTitle)}", dataTypes, appModelResolver);
JournalEntryTitle?.Validate($"{propertyName}.{nameof(JournalEntryTitle)}", dataTypes, appModelResolver);

foreach (var classification in CaseFileClassifications ?? [])
{
classification.Validate($"{propertyName}.{nameof(CaseFileClassifications)}");
}
}
}

Expand Down Expand Up @@ -411,6 +423,13 @@
[JsonPropertyName("filename")]
public string? Filename { get; set; }

/// <summary>
/// Optional override for the document format code (e.g. <c>PDF/A</c>) recorded in the arkivmelding.xml
/// (<c>dokumentobjekt.format.kode</c>). If not specified, the dotless file extension is used.
/// </summary>
[JsonPropertyName("formatCode")]
public string? FormatCode { get; set; }

/// <summary>
/// Internal validation based on the requirements of <see cref="FiksArkivDefaultPayloadGenerator"/>
/// </summary>
Expand All @@ -420,7 +439,7 @@
throw new FiksArkivConfigurationException(
$"{propertyName}.{nameof(DataType)} configuration is required, but missing."
);
if (dataTypes.Any(x => x.Id == DataType) is false)

Check warning on line 442 in src/Altinn.App.Clients.Fiks/FiksArkiv/Models/FiksArkivSettings.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the unnecessary Boolean literal(s).

See more on https://sonarcloud.io/project/issues?id=Altinn_app-lib-dotnet&issues=AZ5ProE7pn65xSQ3zX65&open=AZ5ProE7pn65xSQ3zX65&pullRequest=1765
throw new FiksArkivConfigurationException(
$"{propertyName}.{nameof(DataType)} mismatch with application data types: {DataType}"
);
Expand All @@ -429,6 +448,11 @@
throw new FiksArkivConfigurationException(
$"{propertyName}.{nameof(Filename)} configuration is required, but missing."
);

if (FormatCode is not null && string.IsNullOrWhiteSpace(FormatCode))
throw new FiksArkivConfigurationException(
$"{propertyName}.{nameof(FormatCode)} cannot be empty or whitespace if specified."
);
}

/// <summary>
Expand All @@ -437,3 +461,53 @@
public string GetFilenameOrDefault(string defaultExtension = "xml") =>
!string.IsNullOrWhiteSpace(Filename) ? Filename : $"{DataType}.{defaultExtension.TrimStart('.')}";
}

/// <summary>
/// Represents a single classification (klassifikasjon) entry attached to the saksmappe (case file)
/// in the generated arkivmelding.xml.
/// </summary>
public sealed record FiksArkivClassification
{
/// <summary>
/// The identifier of the classification system this entry belongs to (klassifikasjonssystemID).
/// </summary>
[JsonPropertyName("systemId")]
public required string SystemId { get; set; }

/// <summary>
/// The identifier of the class within the classification system (klasseID).
/// </summary>
[JsonPropertyName("classificationId")]
public required string ClassificationId { get; set; }

/// <summary>
/// A human-readable title for the classification entry (tittel).
/// </summary>
[JsonPropertyName("title")]
public required string Title { get; set; }

/// <summary>
/// Optional flag indicating that the classification is restricted (erSkjermet).
/// Leave <c>null</c> to omit the property from the resulting XML.
/// </summary>
[JsonPropertyName("isRestricted")]
public bool? IsRestricted { get; set; }

internal void Validate(string propertyName)
{
if (string.IsNullOrWhiteSpace(SystemId))
throw new FiksArkivConfigurationException(
$"{propertyName}.{nameof(SystemId)} configuration is required, but missing."
);

if (string.IsNullOrWhiteSpace(ClassificationId))
throw new FiksArkivConfigurationException(
$"{propertyName}.{nameof(ClassificationId)} configuration is required, but missing."
);

if (string.IsNullOrWhiteSpace(Title))
throw new FiksArkivConfigurationException(
$"{propertyName}.{nameof(Title)} configuration is required, but missing."
);
}
}
Loading
Loading