Skip to content

Commit

Permalink
Merge pull request #25 from Worth-NL/feature/OpenZaak_2_0_Upgrade
Browse files Browse the repository at this point in the history
Feature/open zaak 2 0 upgrade
  • Loading branch information
Thomas-M-Krystyan authored Apr 23, 2024
2 parents 881f6b2 + 66f1476 commit 8c18bfc
Show file tree
Hide file tree
Showing 15 changed files with 187 additions and 109 deletions.
2 changes: 1 addition & 1 deletion Documentation/OMC - Documentation.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# OMC Documentation

v.1.6.8
v.1.6.9

© 2024, Worth Systems.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ namespace EventsHandler.Configuration
/// </summary>
public sealed record WebApiConfiguration
{
/// <inheritdoc cref="IConfiguration"/>
internal IConfiguration AppSettings { get; }

/// <summary>
/// Gets the configuration for OMC (internal) system.
/// </summary>
Expand All @@ -25,9 +28,15 @@ public sealed record WebApiConfiguration
/// <summary>
/// Initializes a new instance of the <see cref="WebApiConfiguration"/> class.
/// </summary>
/// <param name="appSettings">The application configurations stored in "appsettings.json" file.</param>
/// <param name="loaderContext">The strategy context using a specific data provider configuration loader.</param>
public WebApiConfiguration(ILoadersContext loaderContext) // NOTE: The only constructor to be used with Dependency Injection
public WebApiConfiguration(
IConfiguration appSettings,
ILoadersContext loaderContext) // NOTE: The only constructor to be used with Dependency Injection
{
// Mapping configurations from "appsettings.json" file => because these should be always accessible, regardless ILoadingService (in ILoadersContext)
this.AppSettings = appSettings;

// Recreating structure of "appsettings.json" or "secrets.json" files to use them later as objects
this.OMC = new OmcComponent(loaderContext, nameof(this.OMC));
this.User = new UserComponent(loaderContext, nameof(this.User));
Expand Down
2 changes: 1 addition & 1 deletion EventsHandler/Api/EventsHandler/Constants/DefaultValues.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ internal static class ApiController
{
internal const string Route = "[controller]";

internal const string Version = "1.68";
internal const string Version = "1.69";
}
#endregion

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ internal static LogLevel GetApplicationInsightsLogLevel(this IConfiguration conf
internal static bool IsEncryptionAsymmetric(this IConfiguration configuration)
=> configuration.GetValue<bool>("Encryption:IsAsymmetric");

internal static bool UseNewOpenKlant(this IConfiguration configuration)
=> configuration.GetValue<bool>("Features:UseNewOpenKlant");

/// <summary>
/// Gets the <see langword="string"/> value from the configuration.
/// </summary>
Expand Down
22 changes: 17 additions & 5 deletions EventsHandler/Api/EventsHandler/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
using SecretsManager.Services.Authentication.Encryptions.Strategy.Interfaces;
using Swashbuckle.AspNetCore.Filters;
using System.Reflection;
using QueryContext = EventsHandler.Services.DataQuerying.ApiDataQuery.QueryContext;

namespace EventsHandler
{
Expand Down Expand Up @@ -187,27 +188,37 @@ private static WebApplicationBuilder AddNetServices(this WebApplicationBuilder b
/// <returns>Partially-configured <see cref="WebApplicationBuilder"/> with custom services.</returns>
private static WebApplicationBuilder AddCustomServices(this WebApplicationBuilder builder)
{
// Configurations
builder.Services.AddSingleton<WebApiConfiguration>();

builder.RegisterEncryptionStrategy();
builder.Services.RegisterLoadingStrategies();
builder.Services.AddSingleton<ITelemetryInitializer, AzureTelemetryService>();

// Business logic
builder.Services.AddSingleton<IValidationService<NotificationEvent>, NotificationValidator>();
builder.Services.AddSingleton<IDetailsBuilder, DetailsBuilder>();
builder.Services.AddSingleton<ISerializationService, SpecificSerializer>();
builder.Services.AddSingleton<IProcessingService<NotificationEvent>, NotifyProcessor>();
builder.Services.AddSingleton<ITemplatesService<TemplateResponse, NotificationEvent>, NotifyTemplatesAnalyzer>();
builder.Services.AddSingleton<ISendingService<NotificationEvent, NotifyData>, NotifySender>();
builder.Services.RegisterNotifyStrategies();

// Queries and HTTP resources
builder.Services.AddSingleton<IDataQueryService<NotificationEvent>, ApiDataQuery>();
builder.Services.AddSingleton<IQueryContext, QueryContext>();
builder.Services.AddSingleton<IHttpSupplierService, JwtHttpSupplier>();
builder.Services.RegisterClientFactories();
builder.Services.AddSingleton<ITemplatesService<TemplateResponse, NotificationEvent>, NotifyTemplatesAnalyzer>();
builder.Services.AddSingleton<ISendingService<NotificationEvent, NotifyData>, NotifySender>();

// Feedback and telemetry
builder.Services.AddSingleton<ITelemetryInitializer, AzureTelemetryService>();
builder.Services.AddSingleton<ITelemetryService, ContactRegistration>();

// User Interaction
builder.Services.RegisterResponders();
builder.Services.AddSingleton<IDetailsBuilder, DetailsBuilder>();

return builder;
}

#region Aggregated registrations
private static void RegisterEncryptionStrategy(this WebApplicationBuilder builder)
{
// Strategies
Expand Down Expand Up @@ -259,6 +270,7 @@ private static void RegisterResponders(this IServiceCollection services)
// Explicit interfaces
services.AddSingleton<IRespondingService<ProcessingResult, string>, NotifyResponder>();
}
#endregion

/// <summary>
/// Configures the HTTP pipeline with middlewares.
Expand Down
152 changes: 67 additions & 85 deletions EventsHandler/Api/EventsHandler/Services/DataQuerying/ApiDataQuery.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// © 2023, Worth Systems.

using EventsHandler.Behaviors.Mapping.Models.Interfaces;
using EventsHandler.Behaviors.Mapping.Models.POCOs.NotificatieApi;
using EventsHandler.Behaviors.Mapping.Models.POCOs.OpenKlant;
using EventsHandler.Behaviors.Mapping.Models.POCOs.OpenZaak;
Expand All @@ -17,157 +16,140 @@ namespace EventsHandler.Services.DataQuerying
/// <inheritdoc cref="IDataQueryService{TModel}"/>
internal sealed class ApiDataQuery : IDataQueryService<NotificationEvent>
{
private readonly ISerializationService _serializer;
private readonly IHttpSupplierService _httpSupplier;

private QueryContext? _notificationBuilder;
private readonly IQueryContext _queryContext;

/// <inheritdoc cref="IDataQueryService{TModel}.HttpSupplier"/>
IHttpSupplierService IDataQueryService<NotificationEvent>.HttpSupplier => this._httpSupplier;
public IHttpSupplierService HttpSupplier { get; }

/// <summary>
/// Initializes a new instance of the <see cref="ApiDataQuery"/> class.
/// </summary>
public ApiDataQuery(
ISerializationService serializer,
IHttpSupplierService httpSupplier)
public ApiDataQuery(IQueryContext queryContext, IHttpSupplierService httpSupplier)
{
this._serializer = serializer;
this._httpSupplier = httpSupplier;
this._queryContext = queryContext;

this.HttpSupplier = httpSupplier;
}

/// <inheritdoc cref="IDataQueryService{TModel}.From(TModel)"/>
QueryContext IDataQueryService<NotificationEvent>.From(NotificationEvent notification)
IQueryContext IDataQueryService<NotificationEvent>.From(NotificationEvent notification)
{
// To optimize the workflow keep the notification builder cached
if (this._notificationBuilder == null)
{
return this._notificationBuilder ??=
new QueryContext(this._httpSupplier.Configuration, this._serializer, this._httpSupplier, notification);
}

// Update only the current notification in cached builder
this._notificationBuilder.Notification = notification;
this._queryContext.Notification = notification;

return this._notificationBuilder;
return this._queryContext;
}

/// <summary>
/// The nested builder operating on <see cref="NotificationEvent"/>.
/// </summary>
internal sealed class QueryContext // TODO: Introduce service "IQueryContext" here
/// <inheritdoc cref="IQueryContext"/>
internal sealed class QueryContext : IQueryContext
{
private readonly WebApiConfiguration _configuration;
private readonly ISerializationService _serializer;
private readonly IHttpSupplierService _httpSupplier;

/// <summary>
/// The notification from "Notificatie API" Web service.
/// </summary>
internal NotificationEvent Notification { private get; set; }
/// <inheritdoc cref="IQueryContext.Notification"/>
NotificationEvent IQueryContext.Notification { get; set; }

/// <summary>
/// Initializes a new instance of the <see cref="ApiDataQuery"/> nested class.
/// </summary>
internal QueryContext(
public QueryContext(
WebApiConfiguration configuration,
ISerializationService serializer,
IHttpSupplierService httpSupplier,
NotificationEvent notification)
IHttpSupplierService httpSupplier)
{
this._configuration = configuration;
this._serializer = serializer;
this._httpSupplier = httpSupplier;

this.Notification = notification;
}

/// <summary>
/// Sends the <see cref="HttpMethods.Get"/> request to the specified URI and deserializes received JSON result.
/// </summary>
#region General HTTP methods
/// <inheritdoc cref="IQueryContext.ProcessGetAsync{TModel}(HttpClientTypes, Uri, string)"/>
/// <exception cref="InvalidOperationException"/>
/// <exception cref="HttpRequestException"/>
internal async Task<TModel> ProcessGetAsync<TModel>(HttpClientTypes httpsClientType, Uri uri, string fallbackErrorMessage)
where TModel : struct, IJsonSerializable
async Task<TModel> IQueryContext.ProcessGetAsync<TModel>(HttpClientTypes httpsClientType, Uri uri, string fallbackErrorMessage)
{
string organizationId = this.Notification.GetOrganizationId();
string organizationId = ((IQueryContext)this).Notification.GetOrganizationId();

(bool isSuccess, string jsonResult) = await this._httpSupplier.GetAsync(httpsClientType, organizationId, uri);

return isSuccess ? this._serializer.Deserialize<TModel>(jsonResult)
: throw new HttpRequestException($"{fallbackErrorMessage} | URI: {uri} | JSON response: {jsonResult}");
}

/// <summary>
/// Sends the <see cref="HttpMethods.Post"/> request to the specified URI and deserializes received JSON result.
/// </summary>
/// <inheritdoc cref="IQueryContext.ProcessPostAsync{TModel}(HttpClientTypes, Uri, HttpContent, string)"/>
/// <exception cref="InvalidOperationException"/>
/// <exception cref="HttpRequestException"/>
internal async Task<TModel> ProcessPostAsync<TModel>(HttpClientTypes httpsClientType, Uri uri, HttpContent body, string fallbackErrorMessage)
where TModel : struct, IJsonSerializable
async Task<TModel> IQueryContext.ProcessPostAsync<TModel>(HttpClientTypes httpsClientType, Uri uri, HttpContent body, string fallbackErrorMessage)
{
string organizationId = this.Notification.GetOrganizationId();
string organizationId = ((IQueryContext)this).Notification.GetOrganizationId();

(bool isSuccess, string jsonResult) = await this._httpSupplier.PostAsync(httpsClientType, organizationId, uri, body);

return isSuccess ? this._serializer.Deserialize<TModel>(jsonResult)
: throw new HttpRequestException($"{fallbackErrorMessage} | URI: {uri} | JSON response: {jsonResult}");
}
#endregion

#region Internal query methods
/// <summary>
/// Gets the <see cref="MainObject"/> from "OpenZaak" Web service.
/// </summary>
internal async Task<MainObject> GetMainObjectAsync() // TODO: This method is not yet used
{
return await ProcessGetAsync<MainObject>(HttpClientTypes.Data, this.Notification.MainObject, Resources.HttpRequest_ERROR_NoMainObject);
}

/// <summary>
/// Gets the <see cref="Case"/> from "OpenZaak" Web service.
/// </summary>
internal async Task<Case> GetCaseAsync()
/// <inheritdoc cref="IQueryContext.GetCaseAsync()"/>
/// <exception cref="InvalidOperationException"/>
/// <exception cref="HttpRequestException"/>
async Task<Case> IQueryContext.GetCaseAsync()
{
return await ProcessGetAsync<Case>(HttpClientTypes.Data, await GetCaseTypeAsync(), Resources.HttpRequest_ERROR_NoCase);
return await ((IQueryContext)this).ProcessGetAsync<Case>(HttpClientTypes.Data, await GetCaseTypeAsync(), Resources.HttpRequest_ERROR_NoCase);
}

/// <summary>
/// Gets the details of a specific citizen from "OpenKlant" Web service.
/// </summary>
internal async Task<CitizenDetails> GetCitizenDetailsAsync()
/// <inheritdoc cref="IQueryContext.GetCitizenDetailsAsync()"/>
/// <exception cref="InvalidOperationException"/>
/// <exception cref="HttpRequestException"/>
async Task<CitizenDetails> IQueryContext.GetCitizenDetailsAsync()
{
// Predefined URL components
string citizensEndpoint = $"https://{GetSpecificOpenKlantDomain()}/klanten/api/v1/klanten";

string citizensEndpoint;
// Request URL
Uri citizenByBsnUri = new($"{citizensEndpoint}?subjectNatuurlijkPersoon__inpBsn={await GetBsnNumberAsync()}");

return await ProcessGetAsync<CitizenDetails>(HttpClientTypes.Data, citizenByBsnUri, Resources.HttpRequest_ERROR_NoCitizenDetails);
Uri citizenByBsnUri;

if (!this._configuration.AppSettings.UseNewOpenKlant())
{
// Open Klant 1.0
citizensEndpoint = $"https://{GetSpecificOpenKlantDomain()}/klanten/api/v1/klanten";
citizenByBsnUri = new Uri($"{citizensEndpoint}?subjectNatuurlijkPersoon__inpBsn={await GetBsnNumberAsync()}");
}
else
{
// Open Klant 2.0
citizensEndpoint = $"https://{GetSpecificOpenKlantDomain()}/"; // TODO: To be finished
citizenByBsnUri = new Uri(citizensEndpoint); // TODO: To be finished
}

return await ((IQueryContext)this).ProcessGetAsync<CitizenDetails>(HttpClientTypes.Data, citizenByBsnUri, Resources.HttpRequest_ERROR_NoCitizenDetails);
}

/// <summary>
/// Gets the status(es) of the specific <see cref="Case"/> from "OpenZaak" Web service.
/// </summary>
internal async Task<CaseStatuses> GetCaseStatusesAsync()
/// <inheritdoc cref="IQueryContext.GetCaseStatusesAsync()"/>
/// <exception cref="InvalidOperationException"/>
/// <exception cref="HttpRequestException"/>
async Task<CaseStatuses> IQueryContext.GetCaseStatusesAsync()
{
// Predefined URL components
string statusesEndpoint = $"https://{GetSpecificOpenZaakDomain()}/zaken/api/v1/statussen";

// Request URL
Uri caseStatuses = new($"{statusesEndpoint}?zaak={this.Notification.MainObject}");
Uri caseStatuses = new($"{statusesEndpoint}?zaak={((IQueryContext)this).Notification.MainObject}");

return await ProcessGetAsync<CaseStatuses>(HttpClientTypes.Data, caseStatuses, Resources.HttpRequest_ERROR_NoCaseStatuses);
return await ((IQueryContext)this).ProcessGetAsync<CaseStatuses>(HttpClientTypes.Data, caseStatuses, Resources.HttpRequest_ERROR_NoCaseStatuses);
}

/// <summary>
/// Gets the type of <see cref="CaseStatus"/>.
/// </summary>
internal async Task<CaseStatusType> GetLastCaseStatusTypeAsync(CaseStatuses statuses)
/// <inheritdoc cref="IQueryContext.GetLastCaseStatusTypeAsync(CaseStatuses)"/>
/// <exception cref="InvalidOperationException"/>
/// <exception cref="HttpRequestException"/>
async Task<CaseStatusType> IQueryContext.GetLastCaseStatusTypeAsync(CaseStatuses statuses)
{
// Request URL
Uri lastStatusTypeUri = statuses.LastStatus().Type;

return await ProcessGetAsync<CaseStatusType>(HttpClientTypes.Data, lastStatusTypeUri, Resources.HttpRequest_ERROR_NoCaseStatusType);
return await ((IQueryContext)this).ProcessGetAsync<CaseStatusType>(HttpClientTypes.Data, lastStatusTypeUri, Resources.HttpRequest_ERROR_NoCaseStatusType);
}
#endregion

Expand All @@ -177,15 +159,15 @@ internal async Task<CaseStatusType> GetLastCaseStatusTypeAsync(CaseStatuses stat
/// </summary>
private async Task<Uri> GetCaseTypeAsync()
{
return this.Notification.Attributes.CaseType ?? (await GetCaseDetailsAsync()).CaseType;
return ((IQueryContext)this).Notification.Attributes.CaseType ?? (await GetCaseDetailsAsync()).CaseType;
}

/// <summary>
/// Gets the <see cref="Case"/> details from "OpenZaak" Web service.
/// </summary>
private async Task<CaseDetails> GetCaseDetailsAsync()
{
return await ProcessGetAsync<CaseDetails>(HttpClientTypes.Data, this.Notification.MainObject, Resources.HttpRequest_ERROR_NoCaseDetails);
return await ((IQueryContext)this).ProcessGetAsync<CaseDetails>(HttpClientTypes.Data, ((IQueryContext)this).Notification.MainObject, Resources.HttpRequest_ERROR_NoCaseDetails);
}

/// <summary>
Expand All @@ -203,9 +185,9 @@ private async Task<CaseRoles> GetCaseRoleAsync()
const string roleType = "natuurlijk_persoon";

// Request URL
Uri caseWithRoleUri = new($"{rolesEndpoint}?zaak={this.Notification.MainObject}&betrokkeneType={roleType}");
Uri caseWithRoleUri = new($"{rolesEndpoint}?zaak={((IQueryContext)this).Notification.MainObject}&betrokkeneType={roleType}");

return await ProcessGetAsync<CaseRoles>(HttpClientTypes.Data, caseWithRoleUri, Resources.HttpRequest_ERROR_NoCaseRole);
return await ((IQueryContext)this).ProcessGetAsync<CaseRoles>(HttpClientTypes.Data, caseWithRoleUri, Resources.HttpRequest_ERROR_NoCaseRole);
}
#endregion

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

using EventsHandler.Behaviors.Mapping.Models.Interfaces;
using EventsHandler.Services.DataReceiving.Interfaces;
using static EventsHandler.Services.DataQuerying.ApiDataQuery;

namespace EventsHandler.Services.DataQuerying.Interfaces
{
Expand All @@ -18,6 +17,6 @@ public interface IDataQueryService<in TModel>
/// <summary>
/// Gets the query context of <see cref="IDataQueryService{TModel}"/> or sets it first if not yet existing.
/// </summary>
internal QueryContext From(TModel model);
internal IQueryContext From(TModel model);
}
}
Loading

0 comments on commit 8c18bfc

Please sign in to comment.