diff --git a/Documentation/OMC - Documentation.md b/Documentation/OMC - Documentation.md
index 598573f8..58e15c09 100644
--- a/Documentation/OMC - Documentation.md
+++ b/Documentation/OMC - Documentation.md
@@ -1,15 +1,190 @@
-# **OMC** Documentation
+
OMC Documentation
-v.1.11.0
+v.1.11.3
-© 2024, Worth Systems.
+© 2023-2024, Worth Systems.
---
-# 1. Introduction
+
+Table of contents
+
+1. [Introduction](#introduction)
+
+ * [Open services](#openServices_list)
+ * [Notify](#notify_list)
+
+ - 1.1. [Swagger UI](#swagger_ui)
+
+ - 1.1.1. [Using web browser](#web_browser)
+
+ - 1.1.2. [Using IDE (Visual Studio)](#visual_studio)
+
+ - 1.1.2.1. [Customizing profile](#custom_lanunchSettings_profile)
+
+ - 1.1.2.2. [Running profile](#running_profile)
+
+ - 1.2. [Docker](#docker)
+
+2. [Architecture](#architecture)
+
+3. [Setup](#setup)
+
+ - 3.1. [Configurations](#configurations)
+
+ - 3.1.1. [appsettings.json](#appsettings)
+
+ - 3.1.1.1. [Example](#appsettings_example)
+
+ - 3.1.2. [Environment variables](#environment_variables)
+
+ - 3.1.2.1. [Example](#environment_variables_example)
+
+ - 3.1.2.2. [Get environment variables](#get_environment_variables)
+
+ - 3.1.2.3. [Set environment variables](#set_environment_variables)
+
+ - 3.1.2.4. [Using HELM Charts](#helm_charts)
+
+4. [Authorization and authentication](#authorization)
+
+ - 4.1. [JSON Web Tokens](#jwt_tokens)
+
+ - 4.1.1. [Required components](#jwt_required_components)
+
+ - 4.1.1.1. [Header (algorithm + type)](#jwt_header)
+
+ - 4.1.1.2. [Payload (claims)](#jwt_claims)
+
+ - 4.1.1.3. [Signature (secret)](#jwt_secret)
+
+ - 4.1.2. [Mapping of JWT claims from environment variables](#jwt_mapping_environment_variables)
+
+ - 4.1.3. [Using generated JSON Web Token (JWT)](#jwt_generating)
+
+ - 4.1.3.1. [Postman (authorization)](#postman_authorization)
+
+ - 4.1.3.2. [Swagger UI (authorization)](#swagger_ui_authorization)
+
+5. [OMC Workflow](#omc_workflow)
+
+ - 5.1. [Versions](#workflow_versions)
+
+ - 5.1.1. [Dependencies](#workflow_dependencies)
+
+ - 5.1.1.1. [OMC workflow v1 `(default)`](#omc_workflow_v1)
+
+ - 5.1.1.1. [OMC workflow v2](#omc_workflow_v2)
+
+ - 5.2. [Scenarios](#scenarios)
+
+ - 5.2.1. [General introduction](#scenarios_general_introduction)
+
+ - 5.2.1.1. [Notification](#scenarios_general_notification)
+
+ - 5.2.1.2. [Environment variables](#scenarios_general_environment_variables)
+
+ - 5.2.1.3. [Requirements](#scenarios_general_requirements)
+
+ - 5.2.1.4. [Template placeholders](#scenarios_general_template_placeholders)
+
+ - [Examples](#scenarios_examples)
+
+ - 5.2.2. [Case Created](#case_created)
+
+ - 5.2.2.1. [Notification](#case_created_notification)
+
+ - 5.2.2.2. [Environment variables](#case_created_environment_variables)
+
+ - 5.2.2.3. [Requirements](#case_created_requirements)
+
+ - 5.2.2.4. [Template placeholders](#case_created_template_placeholders)
+
+ - 5.2.3. [Case Updated](#case_updated)
+
+ - 5.2.3.1. [Notification](#case_updated_notification)
+
+ - 5.2.3.2. [Environment variables](#case_updated_environment_variables)
+
+ - 5.2.3.3. [Requirements](#case_updated_requirements)
+
+ - 5.2.3.4. [Template placeholders](#case_updated_template_placeholders)
+
+ - 5.2.4. [Case Closed](#case_closed)
+
+ - 5.2.4.1. [Notification](#case_closed_notification)
+
+ - 5.2.4.2. [Environment variables](#case_closed_environment_variables)
+
+ - 5.2.4.3. [Requirements](#case_closed_requirements)
+
+ - 5.2.4.4. [Template placeholders](#case_closed_template_placeholders)
+
+ - 5.2.5. [Task Assigned](#task_assigned)
+
+ - 5.2.5.1. [Notification](#task_assigned_notification)
+
+ - 5.2.5.2. [Environment variables](#task_assigned_environment_variables)
+
+ - 5.2.5.3. [Requirements](#task_assigned_requirements)
+
+ - 5.2.5.4. [Template placeholders](#task_assigned_template_placeholders)
+
+ - 5.2.6. [Decision Made](#decision_made)
+
+ - 5.2.6.1. [Notification](#decision_made_notification)
+
+ - 5.2.6.2. [Environment variables](#decision_made_environment_variables)
+
+ - 5.2.6.3. [Requirements](#decision_made_requirements)
+
+ - 5.2.6.4. [Template placeholders](#decision_made_template_placeholders)
+
+ - 5.2.7. [Message Received](#message_received)
+
+ - 5.2.7.1. [Notification](#message_received_notification)
+
+ - 5.2.7.2. [Environment variables](#message_received_environment_variables)
+
+ - 5.2.7.3. [Requirements](#message_received_requirements)
+
+ - 5.2.7.4. [Template placeholders](#message_received_template_placeholders)
+
+ - 5.2.99. [Not Implemented](#not_implemented_scenario)
+
+6. [Errors](#errors)
+
+ - 6.1. [Events Controller](#errors_events_controller)
+
+ - 6.1.1. [Possible errors](#errors_events_controller_possible_errors)
+
+ - 6.2. [Notify Controller](#errors_notify_controller)
+
+ - 6.2.1. [Possible errors](#errors_notify_controller_possible_errors)
+
+ - 6.3. [Test Controller](#errors_test_controller)
+
+ - 6.3.1. [Testing Notify](#errors_test_controller_notify)
+
+ - 6.3.1.1. [Possible errors](#errors_test_controller_notify_possible_errors)
+
+ a) [Common for SendEmail + SendSms](#errors_test_controller_notify_common)
+
+ b) [SendEmail](#errors_test_controller_notify_common_sendEmail)
+
+ c) [SendSms](#errors_test_controller_notify_common_sendSms)
+
+ - 6.3.2. [Testing Open services](#errors_test_controller_open)
+
+ - 6.3.2.1. [Possible errors](#errors_test_controller_open_possible_errors)
+
+---
+1. Introduction
+
+[Go back](#start)
**OMC (Output Management Component)** is a central point and the common hub of the communication workflow between third parties software such as:
- Open services (repositories):
+Open services (repositories)
- [**Open Notificaties**](https://github.com/open-zaak/open-notificaties) (Web API service)
- [**Open Zaak**](https://github.com/open-zaak/open-zaak) (Web API service)
@@ -19,7 +194,7 @@ v.1.11.0
- [**ObjectTypen**](https://github.com/maykinmedia/objecttypes-api) (Web API service)
- [**Klantinteracties**](https://vng-realisatie.github.io/klantinteracties/) (Web API service)
- Notify:
+Notify
- [**Notify NL**](https://github.com/Worth-NL/notifications-api) (Web API service) => based on [**Notify UK**](https://www.notifications.service.gov.uk/)
@@ -32,19 +207,19 @@ v.1.11.0
> **NOTE:** Different versions of these external API services are handled by, so-called "[OMC Workflows](#workflow_versions)".
-## 1.1. Swagger UI
+1.1. Swagger UI
Since the **OMC** project is just an API, it would not have any user-friendly graphic representation if used as a standalone RESTful ASP.NET Web API project.
-That's why **ASP.NET** projects are usually exposing a UI presentation layer for the convenience of future users (usually developers). To achieve this effect, we are using so called [Swagger UI](https://swagger.io/tools/swagger-ui/), a standardized **HTML**/**CSS**/**JavaScript**-based suite of tools and assets made to generate visualized API endpoints, API documentation, data models schema, data validation, interaction with user (API responses), and other helpful hints on how to use the certain API.
+That's why **ASP.NET** projects are usually exposing a UI presentation layer for the convenience of future users (usually developers). To achieve this effect, we are using so called [Swagger UI](https://swagger.io/tools/swagger_ui/), a standardized **HTML**/**CSS**/**JavaScript**-based suite of tools and assets made to generate visualized API endpoints, API documentation, data models schema, data validation, interaction with user (API responses), and other helpful hints on how to use the certain API.
**Swagger UI** can be accessed just like a regular webpage, or when you are starting your project in your IDE (preferably **Visual Studio**).
![Invalid base URL - Error](images/swagger_ui_example.png)
-**NOTE**: Check the section dedicated to [requests authorization](#swagger-ui-authorization) when using **Swagger UI**.
+**NOTE**: Check the section dedicated to [requests authorization](#swagger_ui_authorization) when using **Swagger UI**.
-### 1.1.1. Using web browser
+1.1.1. Using web browser
The URL to **Swagger UI** can be recreated in the following way:
@@ -55,7 +230,7 @@ For example: https://omc.acc.notifynl.nl/swagger/index.html
\* Usually https
\** Where your **OMC** Web API application is deployed
-### 1.1.2. Using IDE (Visual Studio)
+1.1.2. Using IDE (Visual Studio)
To run the application locally (using **Visual Studio**) select one of the `launchSettings.json` **profiles** to start **Swagger UI** page in your browser (which will be using `/localhost:...` address).
@@ -71,7 +246,7 @@ And all of them have **Swagger UI** specified as the default start option.
> **NOTE:** In this example application will start in "Development" mode.
- Customizing profile:
+1.1.2.1. Customizing profile
> Full content of `launchSettings.json` file.
@@ -194,11 +369,11 @@ The developer can create more than one launch profile:
![Multiple custom launch profiles - launchSettings.json](images/launchSettings_many_custom.png)
-#### Running profile:
+1.1.2.2. Running profile
![Invalid base URL - Error](images/launchProfiles_full.png)
-## 1.2. Docker
+1.2. Docker
- After cloning **OMC** Git repository:
> git@github.com:Worth-NL/NotifyNL-OMC.git
@@ -220,14 +395,18 @@ The command from above is addressing the issue with building **docker image** fr
in order to run an already created **docker container**.
---
-# 2. Architecture
+2. Architecture
+
+[Go back](#start)
[Scenarios](#scenarios) implemented in **OMC** are following _Strategy Design Pattern_, and they are using JSON data deserialized into _POCO (Plain Old CLR Object)_ models, and passed as _DTO (Data Transfer Object)_ models to query services (reflecting the external micro-services architecture of third-party "Open Services"). Query services are aggregated under _IQueryContext_ and its implementation _QueryContext_ - following _Adapter Design Pattern_ thanks to which queries can be agnostic (dependencies resolved internally) and organized within a single testable abstraction, giving the developers access to all available API query methods.
---
-# 3. Required environment variables
+3. Setup
-## 3.1. Different configurations
+[Go back](#start)
+
+3.1. Configurations
**OMC API** and related sub-systems (e.g., **Secrets Manager**) are using two types of configurations:
@@ -242,15 +421,15 @@ Which can also be divided into other two categories:
Easier to monitor, test, modify, and maintain by developers are `appsettings.json`,
but `environment variables` are easier to be adjusted by the end users of **OMC**.
-### 3.1.1. `appsettings.json`
+3.1.1. `appsettings.json`
> Made for public configurations (can be preserved in the code). They are not meant to be changed very often.
![Invalid base URL - Error](images/appsettings.png)
-> **NOTE:** Here are defined settings related to HTTP connection, encryption used for JWT tokens to authorize HTTP requests to / from other Web API services, or default variables defining **OMC** domain setup - adjusting how the generic and agnostic [**Open Services**](#openServices-list) will be utilized.
+> **NOTE:** Here are defined settings related to HTTP connection, encryption used for JWT tokens to authorize HTTP requests to / from other Web API services, or default variables defining **OMC** domain setup - adjusting how the generic and agnostic [**Open Services**](#openServices_list) will be utilized.
- Settings:
+3.1.1.1. Example
> Full content of `appsettings.json` file.
@@ -323,11 +502,11 @@ During the start of the **OMC** application the content of `appsettings.[ASPNETC
> **NOTE:** Sometimes, in the documentation or in the code, when referring to this value a name "application mode(s)" might be used - because this _environment variable_ is usually defining the global setup / behavior of any **.NET** application.
-### 3.1.2. `environment variables`
+3.1.2. Environment variables
> Meant to store sensitive configurations and / or customizable per instances of the **OMC** application).
-**Required variables:**
+3.1.2.1. Example
| Name* | .NET Type | Example | Is sensitive | Validation | Notes |
| --------------------------------------------------- | --------- | --------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -402,7 +581,7 @@ During the start of the **OMC** application the content of `appsettings.[ASPNETC
\* Copy-paste the *environment variable* name and set the value of respective type like showed in the **Example** column from the above.
\** GUID and UUID are representing the same data type in the following format: 8-4-4-4-12 and using Hexadecimal values (0-f). The difference is that UUID is used in cross-platform context, while GUID is the data type used in .NET
-#### 3.1.2.1. How to get some of these environment variables
+3.1.2.2. Get environment variables
`OMC_AUTHORIZATION_JWT_SECRET` - To be generated from any passwords manager. Like other **OMC_AUTHORIZATION_[...]** configurations it's meant to be set by the user.
@@ -412,13 +591,13 @@ During the start of the **OMC** application the content of `appsettings.[ASPNETC
`USER_TEMPLATEIDS_SMS_ZAAKCREATE` - All **Template IDs** (SMS and Email) will be generated (and then you can copy-paste them into environment variables) when the user create (one-by-one) new templates from **NotifyNL** Admin Portal => **Templates** section.
-#### 3.1.2.2. Setting environment variables
+3.1.2.3. Set environment variables
1. On Windows:
![Invalid base URL - Error](images/environment_varibles_windows.png)
-Additionally, environment variables can be also defined in **Visual Studio**'s `launchSettings.json` file. Check the example [here](#custom-lanunchSettings-profile).
+Additionally, environment variables can be also defined in **Visual Studio**'s `launchSettings.json` file. Check the example [here](#custom_lanunchSettings_profile).
2. On Linux:
@@ -428,16 +607,18 @@ Additionally, environment variables can be also defined in **Visual Studio**'s `
> To be finished...
-#### 3.1.2.3. Using HELM Charts
+3.1.2.4. Using HELM Charts
**NotifyNL** and **OMC** are meant to be used with [HELM Charts](https://helm.sh/) (helping to install them on your local machine / server).
-- [NotifyNL HELM Charts (GitHub)](https://github.com/Worth-NL/helm-charts)
+- [NotifyNL HELM Charts (GitHub)](https://github.com/Worth-NL/helm_charts)
-- [OMC HELM Charts (GitHub)](https://github.com/Worth-NL/helm-charts/tree/main/notifynl-omc)
+- [OMC HELM Charts (GitHub)](https://github.com/Worth-NL/helm_charts/tree/main/notifynl-omc)
---
-# 4. Authorization and authentication
+4. Authorization and authentication
+
+[Go back](#start)
All of the API services involved in the notifying process (**OpenServices**, **OMC**, **Notify**) requires some type of authorization and authentication procedure.
@@ -445,7 +626,7 @@ All of the API services involved in the notifying process (**OpenServices**, **O
> The user of **OMC** doesn't have to worry which authorization method will be used behind the hood, as long as you provide valid credentials and specify which version of "OpenServices" [workflow](#workflow_versions) is used.
-## 4.1. JSON Web Tokens
+4.1. JSON Web Tokens
In the normal business workflow **OMC** API will ensure that valid _JWT tokens_ would be used internally (based on the provided credentials (_environment variables_). However, developers testing or maintaining the solution need to generate their own JWT tokens (e.g., to access the **OMC** API endpoints from **Swagger UI** or **Postman**) using one of the following approaches.
@@ -476,18 +657,18 @@ Users can also execute their commands directly in the catalog where **SecretsMan
- Through the external **https://jwt.io** webpage (using the same credentials as those defined in _environment variables_).
-### 4.1.1. Required JSON Web Token (JWT) components
+4.1.1. Required components
> Knowing all required *environment variables* you can fill these claims manually and generate your own JWT tokens without using **Secrets Manager**. This approach might be helpful if you are using **OMC** Web API service only as a Web API service (**Swagger UI**), during testing its functionality from **Postman**, or when using only the **Docker Image**.
-#### 4.1.1.1. Header (algorithm + type)
+
> {
"alg": "HS256",
"typ": "JWT"
}
-#### 4.1.1.2. Payload (claims)
+4.1.1.2. Payload (claims)
> {
"client_id": "",
@@ -499,13 +680,13 @@ Users can also execute their commands directly in the catalog where **SecretsMan
"exp": 0000000000
}
-#### 4.1.1.3. Signature (secret)
+4.1.1.3. Signature (secret)
![JWT Signature](images/jwt_signature.png)
> **NOTE:** To be filled in **https://jwt.io**.
-### 4.1.2. Mapping of JWT claims from environment variables
+4.1.2. Mapping of JWT claims from environment variables
| JWT claims | **OMC** Environment Variables |
| --------------------- | -------------------------------------------- |
@@ -521,16 +702,16 @@ Users can also execute their commands directly in the catalog where **SecretsMan
> **NOTE:** "iat" and "exp" times requires Unix formats of timestamps.
The Unix timestamp can be generated using [Unix converter](https://www.unixtimestamp.com/).
-### 4.1.3. Using generated JSON Web Token (JWT)
+4.1.3. Using generated JSON Web Token (JWT)
-#### 4.1.3.1. Postman
+4.1.3.1. Postman (authorization)
> After generating the JWT token you can copy-paste it in **Postman** to authorize your HTTP requests.
![Postman - Authorization](images/postman_authorization.png)
---
-4.1.3.2. Swagger UI
+4.1.3.2. Swagger UI (authorization)
> If you are using **OMC** **Swagger UI** from browser (graphic interface for **OMC** Web API service) then you need to copy the generated token in the following way:
@@ -539,23 +720,26 @@ The Unix timestamp can be generated using [Unix converter](https://www.unixtimes
And then click "Authorize".
---
-# 5. OMC Workflow
+5. OMC Workflow
+
+[Go back](#start)
![Invalid base URL - Error](images/OMC_Sequence_Chart.png)
> Version of **OMC** <= 1.7.4 (using "[OMC workflow v1](#workflow_dependencies)").
-5.1. Versions of OMC workflows
+5.1. Versions
The **OMC** API is using different configurations and setups to handle multiple complex business cases. Sometimes, it is even required to support multiple versions of the same external API services (which might be very different from each other).
-**Last update:** 03 Oct 2024
+> **NOTE:** The OMC workflow can be changed using a respective _environment variable_ in the section of features.
5.1.1. Dependencies
Here are the details which _workflows_ are using which versions of the external API services:
-#### OMC workflow v1 `(default)`:
+OMC workflow v1 (default)
+
- "OpenNotificaties" v1.6.0
- "OpenZaak" v1.12.1
- "OpenKlant" v1.0.0
@@ -564,7 +748,10 @@ Here are the details which _workflows_ are using which versions of the external
- "ObjectTypen" v2.2.0
- "Contactmomenten" v1.0.0
-#### OMC workflow v2:
+> **NOTE:** This workflow is supporting only _citizens_ (using **BSN** numbers).
+
+OMC workflow v2
+
- "OpenNotificaties" v1.6.0
- "OpenZaak" v1.12.1
- new
"OpenKlant" v2.0.0
@@ -573,13 +760,13 @@ Here are the details which _workflows_ are using which versions of the external
- "ObjectTypen" v2.2.0
- new
"Klantcontacten" v2.0.0
-> **NOTE:** The OMC workflow can be changed using a respective _environment variable_ in the section of features.
+> **NOTE:** This workflow is supporting both _citizens_ (using **BSN** numbers) and _organizations_ (using **KVK** numbers). The term used to describe such different entities is "party".
5.2. Scenarios
List of scenarios and the details how to use them with **OMC** (configurations, template personalizations, environment variables, business logic conditions, etc.).
-### 5.2.1. General introduction
+5.2.1. General introduction
**OMC** "Scenarios" are specific processing workflows, set up in the code to handle certain business requirements: _what_, _when_, _how_, and _which_ to process the "initial notification" received from a subscribed channel from a _message queue_ implemented by **Open Notificaties** Web API service.
@@ -600,7 +787,7 @@ Currently, the following business **scenarios** are implemented:
- Receiving a _decision_
- Receiving a _message_
-#### 5.2.1.1. Notification
+5.2.1.1. Notification
Any **OMC** workflow relies on receiving the (initial) notification event from **Open Notificaties** Web API service to trigger the processing business logic.
@@ -610,16 +797,20 @@ Except of being awaited by **OMC** callback (`[OMC]/events/listen` endpoint) it
Using **Swagger UI** is recommended solution, because of its user-friendly User Interface, documentation of endpoints, parameters, remarks, JSON examples, model schemas, and validation; formatting of API responses is also better than in **Postman**.
-#### 5.2.1.2. Environment variables
+5.2.1.2. Environment variables
To work properly **OMC** always requires these mandatory _environment variables_ to be set:
> **NOTE:** If some environment variable is missing but required by one of the countless scenarios, conditions, or workflows, the **OMC** application will return a readable and user-friendly API response with the name of the missing environment variable. This is the easiest way to figure out what else is required.
+
+
`ASPNETCORE_ENVIRONMENT`
> Used by `[OMC]/events/version` endpoint and to determine which `appsettings[.xxx].json` will be used.
+
+
`OMC_AUTHORIZATION_JWT_SECRET`
`OMC_AUTHORIZATION_JWT_ISSUER`
@@ -634,14 +825,20 @@ To work properly **OMC** always requires these mandatory _environment variables_
> Required to get access to **OMC** and be able to use it. Moreover, **Open Notificaties** Web API service will use this method to make an authorized requests while sending notification events to **OMC**.
+
+
`OMC_API_BASEURL_NOTIFYNL`
> Without this URL notifications would not work.
+
+
`OMC_FEATURES_WORKFLOW_VERSION`
> Without this setting (the version needs to be supported) the **OMC** Web API will not even run and specific implementations of underlying services will not be resolved by _Dependency Injection_ mechanism. By default you can always use `"1"` if you don't know yet which other [OMC Workflow](#workflow_versions) version you should use.
+
+
`USER_AUTHORIZATION_JWT_SECRET`
`USER_AUTHORIZATION_JWT_ISSUER`
@@ -656,6 +853,8 @@ To work properly **OMC** always requires these mandatory _environment variables_
> **JWT authorization** is required by some versions of external API services used in certain [OMC Workflow](#workflow_versions) versions.
+
+
`USER_API_KEY_OPENKLANT` => Required only in certain [OMC Workflow](#workflow_versions) versions
`USER_API_KEY_OBJECTEN`
@@ -666,6 +865,8 @@ To work properly **OMC** always requires these mandatory _environment variables_
> **API key authorization** is required by some versions of external API services used in certain [OMC Workflow](#workflow_versions) versions.
+
+
`USER_DOMAIN_OPENZAAK`
`USER_DOMAIN_OPENKLANT`
@@ -680,6 +881,8 @@ To work properly **OMC** always requires these mandatory _environment variables_
> **Domains** might have different _paths_ (e.g., `domain/something/v1/`) depends on version of external API service used in certain [OMC Workflow](#workflow_versions). For example domains for OpenKlant and ContactMomenten depends on version of **Open Klant** Web API service. Moreover, domains and paths depends on the place where your version of Web API service was deployed (domain) and the way how it is internally structured (paths).
+
+
These _environment variables_ are optional:
`SENTRY_DSN`
@@ -688,21 +891,23 @@ These _environment variables_ are optional:
> Logging and analytics in third-party service ([Sentry.io](https://sentry.io)).
-#### 5.2.1.3. Conditioning
+5.2.1.3. Requirements
To process certain notification the specific internal criteria must be met. Usually, they are some pre-validation (analyzing the "initial notification" received from **Open Notificaties** Web API service), post-validation (to determine the scenario suited for this type of the notification), and whitelisting steps (to ensure that **OMC** should continue processing this type of notification). Sometimes, additional checks have to be performed - which depends on the specific **OMC** scenario.
-#### 5.2.1.4. Template placeholders
+5.2.1.4. Template placeholders
-When everything is already validated, prepared, and processed, the **Notify NL** Web API service needs to receive instruction how to format the upcoming notification. The way how to achieve this is to set up so called "template" (using **Notify NL Admin portal** webpage), define `((placeholders))` in the text (_subject_ and/or _body_) - matching to the ones defined by specific **OMC** scenario, and then use the `ID` of this freshly generated "template" in respective _environment variable_ for **OMC**.
+When everything is already validated, prepared, and processed, the **Notify NL** Web API service needs to receive instruction how to format the upcoming notification. The way how to achieve this is to set up so called "template" (using **Notify NL Admin portal** webpage), define `((placeholders))` in the text (_subject_ and/or _body_) - matching to the ones defined by the specific **OMC** scenario, and then use the `ID` of this freshly generated "template" in respective _environment variable_ for **OMC**.
---
-### 5.2.2. Case Created
+Examples
+
+5.2.2. Case Created
Notifies the respective party (e.g., a citizen or an organization) about the case being open for them. For the residents of The Netherlands the case is related to their unique personal identification number **BSN** (_Burgerservicenummer_), thanks to which their contact details and contact preferrences can be retrieved (whether they want to be notified and which notification method they prefer, e.g. by Email, SMS, etc.).
-#### 5.2.2.1. Notification
+5.2.2.1. Notification
Example of JSON schema:
@@ -722,7 +927,7 @@ Example of JSON schema:
}
```
-#### 5.2.2.2. Environment variables
+5.2.2.2. Environment variables
Required to be set:
@@ -734,13 +939,28 @@ Required to be set:
`USER_WHITELIST_ZAAKCREATE_IDS`
-#### 5.2.2.3. Conditioning
+5.2.2.3. Requirements
+
+- The _initial notification_ has:
+ -- **Action:** Create (`"create"`)
+ -- **Channel:** Cases (`"zaken"`)
+ -- **Resource:** Status (`"status"`)
+
+- The _case_ has 1 _status_ (it was never updated) => this is a new _case_
+
+- The _case type identifier_ (`"zaaktypeIdentificatie"`) has to be **whitelisted** or `"*"` wildcard used (to accept all case types) in respective whitelist _environment variable_
+
+- The notification indication property (`"informeren"`) in _case type_ is set to _true_
-... To be done
+- All **URI**s are valid, source data complete, and **JWT token** or **API keys** correct
-#### 5.2.2.4. Template placeholders
+The notification will be processed and sent!
-Required placeholders names in the template:
+> Otherwise, user will get a meaningful API feedback from **OMC** application explaining what exactly is missing.
+
+5.2.2.4. Template placeholders
+
+Required placeholders names in the **Notify NL** template:
`((klant.voornaam))`
@@ -756,11 +976,11 @@ Required placeholders names in the template:
---
-### 5.2.3. Case Status Updated
+5.2.3. Case Status Updated
Notifies the respective party (e.g., a citizen or an organization) that the status of their case was updated.
-#### 5.2.3.1. Notification
+5.2.3.1. Notification
Example of JSON schema:
@@ -780,7 +1000,7 @@ Example of JSON schema:
}
```
-#### 5.2.3.2. Environment variables
+5.2.3.2. Environment variables
Required to be set:
@@ -792,13 +1012,30 @@ Required to be set:
`USER_WHITELIST_ZAAKUPDATE_IDS`
-#### 5.2.3.3. Conditioning
+5.2.3.3. Requirements
+
+- The _initial notification_ has:
+ -- **Action:** Create (`"create"`)
+ -- **Channel:** Cases (`"zaken"`)
+ -- **Resource:** Status (`"status"`)
+
+- The _case_ has 2+ _statuses_ (it was updated at least once)
+
+- The last _case status_ is not set to final (`"isEindstatus" : false`) => the _status_ of a _case_ is just updated and not yet finalized
-... To be done
+- The _case type identifier_ (`"zaaktypeIdentificatie"`) has to be **whitelisted** or `"*"` wildcard used (to accept all case types) in respective whitelist _environment variable_
-#### 5.2.3.4. Template placeholders
+- The notification indication property (`"informeren"`) in _case type_ is set to _true_
-Required placeholders names in the template:
+- All **URI**s are valid, source data complete, and **JWT token** or **API keys** correct
+
+The notification will be processed and sent!
+
+> Otherwise, user will get a meaningful API feedback from **OMC** application explaining what exactly is missing.
+
+5.2.3.4. Template placeholders
+
+Required placeholders names in the **Notify NL** template:
`((klant.voornaam))`
@@ -818,11 +1055,11 @@ Required placeholders names in the template:
---
-### 5.2.4. Case Closed
+5.2.4. Case Closed
Notifies the respective party (e.g., a citizen or an organization) that their case was closed (e.g., resolved).
-#### 5.2.4.1. Notification
+5.2.4.1. Notification
Example of JSON schema:
@@ -842,7 +1079,7 @@ Example of JSON schema:
}
```
-#### 5.2.4.2. Environment variables
+5.2.4.2. Environment variables
Required to be set:
@@ -854,13 +1091,30 @@ Required to be set:
`USER_WHITELIST_ZAAKCLOSE_IDS`
-#### 5.2.4.3. Conditioning
+5.2.4.3. Requirements
+
+- The _initial notification_ has:
+ -- **Action:** Create (`"create"`)
+ -- **Channel:** Cases (`"zaken"`)
+ -- **Resource:** Status (`"status"`)
-... To be done
+- The _case_ has 2+ _statuses_ (it was updated at least once)
-#### 5.2.4.4. Template placeholders
+- The last _case status_ is set to final (`"isEindstatus" : true`) => the _case_ is closed
-Required placeholders names in the template:
+- The _case type identifier_ (`"zaaktypeIdentificatie"`) has to be **whitelisted** or `"*"` wildcard used (to accept all case types) in respective whitelist _environment variable_
+
+- The notification indication property (`"informeren"`) in _case type_ is set to _true_
+
+- All **URI**s are valid, source data complete, and **JWT token** or **API keys** correct
+
+The notification will be processed and sent!
+
+> Otherwise, user will get a meaningful API feedback from **OMC** application explaining what exactly is missing.
+
+5.2.4.4. Template placeholders
+
+Required placeholders names in the **Notify NL** template:
`((klant.voornaam))`
@@ -880,11 +1134,11 @@ Required placeholders names in the template:
---
-### 5.2.5. Task Assigned
+5.2.5. Task Assigned
Notifies the respective party (e.g., a citizen or an organization) that the new task was assigned to them.
-#### 5.2.5.1. Notification
+5.2.5.1. Notification
Example of JSON schema:
@@ -902,7 +1156,7 @@ Example of JSON schema:
}
```
-#### 5.2.5.2. Environment variables
+5.2.5.2. Environment variables
Required to be set:
@@ -916,13 +1170,34 @@ Required to be set:
`USER_WHITELIST_TASKOBJECTTYPE_UUID`
-#### 5.2.5.3. Conditioning
+5.2.5.3. Requirements
+
+- The _initial notification_ has:
+ -- **Action:** Create (`"create"`)
+ -- **Channel:** Objects (`"objecten"`)
+ -- **Resource:** Object (`"object"`)
+
+- The **GUID** from _object type URI_ (`"objectType"`) in the _initial notification_ has to be **whitelisted** or `"*"` wildcard used (to accept all object types) in respective whitelist _environment variable_. This step will distinguish for which object type the notification is desired (e.g., tasks, messages, etc.)
+
+- The _task_ status (`"status"`) from `record.data` nested object is set to open (`"open"`)
-... To be done
+- The _task_ identification type (`"type"`) from `record.data.identificatie` is set to:
+ -- private person (`"bsn"`)
+ -- or company (`"kvk"`)
-#### 5.2.5.4. Template placeholders
+- The _case type identifier_ (`"zaaktypeIdentificatie"`) has to be **whitelisted** or `"*"` wildcard used (to accept all case types) in respective whitelist _environment variable_
-Required placeholders names in the template:
+- The notification indication property (`"informeren"`) in _case type_ is set to _true_
+
+- All **URI**s are valid, source data complete, and **JWT token** or **API keys** correct
+
+The notification will be processed and sent!
+
+> Otherwise, user will get a meaningful API feedback from **OMC** application explaining what exactly is missing.
+
+5.2.5.4. Template placeholders
+
+Required placeholders names in the **Notify NL** template:
`((klant.voornaam))`
@@ -946,11 +1221,11 @@ Required placeholders names in the template:
---
-### 5.2.6. Decision Made
+5.2.6. Decision Made
Notifies the respective party (e.g., a citizen or an organization) that the decision was made in their case.
-#### 5.2.6.1. Notification
+5.2.6.1. Notification
Example of JSON schema:
@@ -969,7 +1244,7 @@ Example of JSON schema:
}
```
-#### 5.2.6.2. Environment variables
+5.2.6.2. Environment variables
Required to be set:
@@ -987,13 +1262,32 @@ Required to be set:
`USER_VARIABLES_OBJECTEN_MESSAGEOBJECTTYPE_VERSION`
-#### 5.2.6.3. Conditioning
+5.2.6.3. Requirements
+
+- The _initial notification_ has:
+ -- **Action:** Create (`"create"`)
+ -- **Channel:** Objects (`"besluiten"`)
+ -- **Resource:** Object (`"besluitinformatieobject"`)
+
+- The **GUID** from _info object type URI_ (`"informatieobjecttype"`) linked to the _decision_ has to be **whitelisted** or `"*"` wildcard used (to accept all info object types) in respective whitelist _environment variable_
+
+- The _info object status_ (`"status"`) is set to definitive (`"definitief"`)
+
+- The _info object confidentiality_ (`"vertrouwelijkheidaanduiding"`) is set to non-confidential (`"openbaar"`)
+
+- The _case type identifier_ (`"zaaktypeIdentificatie"`) has to be **whitelisted** or `"*"` wildcard used (to accept all case types) in respective whitelist _environment variable_
-... To be done
+- The notification indication property (`"informeren"`) in _case type_ is set to _true_
-#### 5.2.6.4. Template placeholders
+- All **URI**s are valid, source data complete, and **JWT token** or **API keys** correct
-Required placeholders names in the template:
+The notification will be processed and sent!
+
+> Otherwise, user will get a meaningful API feedback from **OMC** application explaining what exactly is missing.
+
+5.2.6.4. Template placeholders
+
+Required placeholders names in the **Notify NL** template:
`((klant.voornaam))`
@@ -1053,11 +1347,11 @@ Required placeholders names in the template:
---
-### 5.2.7. Message Received
+5.2.7. Message Received
Notifies the respective party (e.g., a citizen or an organization) that the message with decision is available on their mailbox.
-#### 5.2.7.1. Notification
+5.2.7.1. Notification
Example of JSON schema:
@@ -1075,7 +1369,7 @@ Example of JSON schema:
}
```
-#### 5.2.7.2. Environment variables
+5.2.7.2. Environment variables
Required to be set:
@@ -1089,13 +1383,26 @@ Required to be set:
`USER_WHITELIST_MESSAGEOBJECTTYPE_UUID`
-#### 5.2.7.3. Conditioning
+5.2.7.3. Requirements
+
+- The _initial notification_ has:
+ -- **Action:** Create (`"create"`)
+ -- **Channel:** Objects (`"objecten"`)
+ -- **Resource:** Object (`"object"`)
+
+- The **GUID** from _object type URI_ (`"objectType"`) in the _initial notification_ has to be **whitelisted** or `"*"` wildcard used (to accept all object types) in respective whitelist _environment variable_. This step will distinguish for which object type the notification is desired (e.g., tasks, messages, etc.)
-... To be done
+- Sending of messages is allowed in respective _environment variable_
-#### 5.2.7.4. Template placeholders
+- All **URI**s are valid, source data complete, and **JWT token** or **API keys** correct
-Required placeholders names in the template:
+The notification will be processed and sent!
+
+> Otherwise, user will get a meaningful API feedback from **OMC** application explaining what exactly is missing.
+
+5.2.7.4. Template placeholders
+
+Required placeholders names in the **Notify NL** template:
`((klant.voornaam))`
@@ -1111,14 +1418,16 @@ Required placeholders names in the template:
---
-### 5.2.99. Not Implemented
+5.2.99. Not Implemented
A special fallback scenario which only role is to report that the provided "initial notification" or conditions are not sufficient to determine a proper **OMC** scenario - to be resolved and used for processing the business logic.
User can expect meaningful API response from **OMC**. This response will have _HTTP Status Code_ (206) that will not trigger **Open Notificaties** Web API service to retry sending the same type of "initial notification" again (which would be pointless and fail again since the **OMC** scenario or new condition are not yet implemented).
---
-# 6. Errors
+6. Errors
+
+[Go back](#start)
List of **validation** (format, requirements), **connectivity** or business logic **processing** errors that you might encounter during accessing **OMC** API endpoints.
@@ -1134,14 +1443,14 @@ List of **validation** (format, requirements), **connectivity** or business logi
![Invalid JWT secret - Error](images/general_jwt_secret_wrong.png)
-## 6.1. Events Controller
+6.1. Events Controller
Endpoints:
- `POST` .../Events/Listen
- `GET` .../Events/Version
-#### 6.1.2. Possible errors
+6.1.1. Possible errors
> HTTP Status Code: 206 Partial Content
@@ -1185,13 +1494,13 @@ Other cases (than not implemented business case scenarios) may raise 501 errors.
This is however highly unlikely and might occur mainly in the development phase.
---
-## 6.2. Notify Controller
+6.2. Notify Controller
Endpoints:
- `POST` .../Notify/Confirm
-#### 6.2.1. Possible errors
+6.2.1. Possible errors
> HTTP Status Code: 400 Bad Request
@@ -1211,9 +1520,9 @@ You woull get the following outcome (separated by pipes):
> **NOTE**: Unfortunately, **OMC** Development Team cannot provide meaningful guidance how the external services were developed or configured.
---
-## 6.3. Test Controller
+6.3. Test Controller
-### 6.3.1. Testing Notify
+6.3.1. Testing Notify
Endpoints:
@@ -1221,9 +1530,9 @@ Endpoints:
- `POST` .../Test/Notify/SendEmail
- `POST` .../Test/Notify/SendSms
-#### 6.3.1.1. Possible errors
+6.3.1.1. Possible errors
-##### a) Common for SendEmail + SendSms:
+a) Common for SendEmail + SendSms
> **HTTP Status Code: 403 Forbidden**
@@ -1259,7 +1568,7 @@ Endpoints:
![Missing required personalization - Error](images/test_notify_personalizationMissingError.png)
-##### b) SendEmail:
+b) SendEmail
- Missing required parameters:
@@ -1281,7 +1590,7 @@ Endpoints:
![Invalid email - Error](images/test_notify_emailInvalidError.png)
-##### c) SendSms:
+c) SendSms
- Missing required parameters:
@@ -1321,12 +1630,12 @@ Endpoints:
![Invalid phone format - Error](images/test_notify_phoneFormatError.png)
-### 6.3.2. Testing Open services
+6.3.2. Testing Open services
Endpoints:
- `POST` .../Test/Open/ContactRegistration
-#### 6.3.2.1. Possible errors
+6.3.2.1. Possible errors
> To be finished...
diff --git a/EventsHandler/Api/EventsHandler/Constants/DefaultValues.cs b/EventsHandler/Api/EventsHandler/Constants/DefaultValues.cs
index 11edbeb9..c9eb9ac3 100644
--- a/EventsHandler/Api/EventsHandler/Constants/DefaultValues.cs
+++ b/EventsHandler/Api/EventsHandler/Constants/DefaultValues.cs
@@ -12,7 +12,7 @@ internal static class ApiController
{
internal const string Route = "[controller]";
- internal const string Version = "1.110";
+ internal const string Version = "1.113";
}
#endregion
diff --git a/EventsHandler/Api/EventsHandler/Extensions/StringExtensions.cs b/EventsHandler/Api/EventsHandler/Extensions/StringExtensions.cs
index 23baf625..9a33657d 100644
--- a/EventsHandler/Api/EventsHandler/Extensions/StringExtensions.cs
+++ b/EventsHandler/Api/EventsHandler/Extensions/StringExtensions.cs
@@ -1,10 +1,10 @@
// © 2024, Worth Systems.
+using EventsHandler.Constants;
using System.Buffers.Text;
using System.Diagnostics.CodeAnalysis;
using System.IO.Compression;
using System.Text;
-using EventsHandler.Constants;
namespace EventsHandler.Extensions
{
diff --git a/EventsHandler/Api/EventsHandler/Mapping/Enums/Objecten/IdTypes.cs b/EventsHandler/Api/EventsHandler/Mapping/Enums/Objecten/IdTypes.cs
index c4d79800..02532209 100644
--- a/EventsHandler/Api/EventsHandler/Mapping/Enums/Objecten/IdTypes.cs
+++ b/EventsHandler/Api/EventsHandler/Mapping/Enums/Objecten/IdTypes.cs
@@ -23,6 +23,12 @@ public enum IdTypes
/// The BSN (citizen service number) type of the .
///
[JsonPropertyName("bsn")]
- Bsn = 1
+ Bsn = 1,
+
+ ///
+ /// The KVK (Chamber of Commerce number) type of the .
+ ///
+ [JsonPropertyName("kvk")]
+ Kvk = 2
}
}
\ No newline at end of file
diff --git a/EventsHandler/Api/EventsHandler/Mapping/Models/POCOs/OpenKlant/v2/PartyResults.cs b/EventsHandler/Api/EventsHandler/Mapping/Models/POCOs/OpenKlant/v2/PartyResults.cs
index eeb7460f..cf4d7c56 100644
--- a/EventsHandler/Api/EventsHandler/Mapping/Models/POCOs/OpenKlant/v2/PartyResults.cs
+++ b/EventsHandler/Api/EventsHandler/Mapping/Models/POCOs/OpenKlant/v2/PartyResults.cs
@@ -52,118 +52,90 @@ public PartyResults()
internal readonly (PartyResult, DistributionChannels, string EmailAddress, string PhoneNumber)
Party(WebApiConfiguration configuration)
{
+ // Validation #1: Results
if (this.Results.IsNullOrEmpty())
{
throw new HttpRequestException(Resources.HttpRequest_ERROR_EmptyPartiesResults);
}
- string fallbackEmailAddress = string.Empty;
- string fallbackPhoneNumber = string.Empty;
PartyResult fallbackEmailOwningParty = default;
PartyResult fallbackPhoneOwningParty = default;
+ DistributionChannels distributionChannel = default;
+ string fallbackEmailAddress = string.Empty;
+ string fallbackPhoneNumber = string.Empty;
// Determine which party result should be returned and match the data
- foreach (PartyResult party in this.Results)
+ foreach (PartyResult partyResult in this.Results)
{
- // VALIDATION: Addresses
- if (party.Expansion.DigitalAddresses.IsNullOrEmpty())
+ // Validation #2: Addresses
+ if (partyResult.Expansion.DigitalAddresses.IsNullOrEmpty())
{
continue; // Do not waste time on processing party data which would be for 100% invalid
}
- Guid prefDigitalAddressId = party.PreferredDigitalAddress.Id;
-
- // Looking which digital address should be used
- foreach (DigitalAddressLong digitalAddress in party.Expansion.DigitalAddresses)
+ // Determine which address is preferred
+ if (IsPreferredFound(configuration, partyResult,
+ ref fallbackEmailOwningParty, ref fallbackPhoneOwningParty, ref distributionChannel,
+ ref fallbackEmailAddress, ref fallbackPhoneNumber))
{
- // Recognize what type of digital address it is
- DistributionChannels distributionChannel =
- DetermineDistributionChannel(digitalAddress, configuration);
-
- // VALIDATION: Distribution channel
- if (distributionChannel is DistributionChannels.Unknown)
- {
- continue; // Any digital address couldn't be found
- }
-
- (string emailAddress, string phoneNumber) =
- DetermineDigitalAddresses(digitalAddress, distributionChannel);
-
- // VALIDATION: e-mail and phone number
- if (emailAddress.IsNullOrEmpty() && phoneNumber.IsNullOrEmpty())
- {
- continue; // Empty results cannot be used anyway
- }
-
- // 1. This address is the preferred one and should be prioritized
- if (prefDigitalAddressId != Guid.Empty &&
- prefDigitalAddressId == digitalAddress.Id)
- {
- return (party, distributionChannel, emailAddress, phoneNumber);
- }
-
- // 2a. This is one of many other addresses to be checked
- if (fallbackEmailAddress.IsNullOrEmpty() && // Only the first encountered one matters
- emailAddress.IsNotNullOrEmpty())
- {
- fallbackEmailAddress = emailAddress;
- fallbackEmailOwningParty = party;
-
- continue; // The e-mail address always has priority over the phone number.
- // If any e-mail address was found during this run then the phone
- // number doesn't matter anymore since it will not be returned anyway
- }
-
- if (fallbackPhoneNumber.IsNullOrEmpty() && // Only the first encountered one matters
- phoneNumber.IsNotNullOrEmpty())
- {
- fallbackPhoneNumber = phoneNumber;
- fallbackPhoneOwningParty = party;
- }
+ return (partyResult, distributionChannel, fallbackEmailAddress, fallbackPhoneNumber);
}
}
- // 2b. FALLBACK APPROACH: If the party's preferred address couldn't be determined
- // the email address has priority and the first encountered one should be returned
- if (fallbackEmailAddress.IsNotNullOrEmpty())
- {
- return (fallbackEmailOwningParty, DistributionChannels.Email,
- EmailAddress: fallbackEmailAddress, PhoneNumber: string.Empty);
- }
-
- // 2c. FALLBACK APPROACH: If the email also couldn't be determined then alternatively
- // the first encountered telephone number (for SMS) should be returned instead
- if (fallbackPhoneNumber.IsNotNullOrEmpty())
- {
- return (fallbackPhoneOwningParty, DistributionChannels.Sms,
- EmailAddress: string.Empty, PhoneNumber: fallbackPhoneNumber);
- }
-
- // 2d. In the case of worst possible scenario, that preferred address couldn't be determined
- // neither any existing email address nor telephone number, then process can't be finished
- throw new HttpRequestException(Resources.HttpRequest_ERROR_NoDigitalAddresses);
+ // Pick any matching address
+ return GetMatchingContactDetails(
+ fallbackEmailOwningParty, fallbackPhoneOwningParty,
+ fallbackEmailAddress, fallbackPhoneNumber);
}
- // TODO: Probably most of this code if not all can be reused by both methods
///
internal static (PartyResult, DistributionChannels, string EmailAddress, string PhoneNumber)
- Party(PartyResult partyResult, WebApiConfiguration configuration)
+ Party(WebApiConfiguration configuration, PartyResult partyResult)
{
- string fallbackEmailAddress = string.Empty;
- string fallbackPhoneNumber = string.Empty;
+ // Validation #1: Addresses
+ if (partyResult.Expansion.DigitalAddresses.IsNullOrEmpty())
+ {
+ throw new HttpRequestException(Resources.HttpRequest_ERROR_NoDigitalAddresses);
+ }
+
PartyResult fallbackEmailOwningParty = default;
PartyResult fallbackPhoneOwningParty = default;
+ DistributionChannels distributionChannel = default;
+ string fallbackEmailAddress = string.Empty;
+ string fallbackPhoneNumber = string.Empty;
+
+ // Determine which address is preferred
+ if (IsPreferredFound(configuration, partyResult,
+ ref fallbackEmailOwningParty, ref fallbackPhoneOwningParty, ref distributionChannel,
+ ref fallbackEmailAddress, ref fallbackPhoneNumber))
+ {
+ return (partyResult, distributionChannel, fallbackEmailAddress, fallbackPhoneNumber);
+ }
+
+ // Pick any matching address
+ return GetMatchingContactDetails(
+ fallbackEmailOwningParty, fallbackPhoneOwningParty,
+ fallbackEmailAddress, fallbackPhoneNumber);
+ }
- Guid prefDigitalAddressId = partyResult.PreferredDigitalAddress.Id;
+ #region Helper methods
+ // NOTE: Checks preferred contact address
+ private static bool IsPreferredFound(WebApiConfiguration configuration, PartyResult party,
+ ref PartyResult fallbackEmailOwningParty,
+ ref PartyResult fallbackPhoneOwningParty,
+ ref DistributionChannels distributionChannel,
+ ref string fallbackEmailAddress,
+ ref string fallbackPhoneNumber)
+ {
+ Guid prefDigitalAddressId = party.PreferredDigitalAddress.Id;
// Looking which digital address should be used
- foreach (DigitalAddressLong digitalAddress in partyResult.Expansion.DigitalAddresses)
+ foreach (DigitalAddressLong digitalAddress in party.Expansion.DigitalAddresses)
{
// Recognize what type of digital address it is
- DistributionChannels distributionChannel =
- DetermineDistributionChannel(digitalAddress, configuration);
+ distributionChannel = DetermineDistributionChannel(digitalAddress, configuration);
- // VALIDATION: Distribution channel
+ // Validation #1: Distribution channel
if (distributionChannel is DistributionChannels.Unknown)
{
continue; // Any digital address couldn't be found
@@ -172,7 +144,7 @@ internal static (PartyResult, DistributionChannels, string EmailAddress, string
(string emailAddress, string phoneNumber) =
DetermineDigitalAddresses(digitalAddress, distributionChannel);
- // VALIDATION: e-mail and phone number
+ // Validation #2: E-mail and phone number
if (emailAddress.IsNullOrEmpty() && phoneNumber.IsNullOrEmpty())
{
continue; // Empty results cannot be used anyway
@@ -182,30 +154,45 @@ internal static (PartyResult, DistributionChannels, string EmailAddress, string
if (prefDigitalAddressId != Guid.Empty &&
prefDigitalAddressId == digitalAddress.Id)
{
- return (partyResult, distributionChannel, emailAddress, phoneNumber);
+ fallbackEmailOwningParty = party;
+ fallbackEmailAddress = emailAddress;
+
+ fallbackPhoneOwningParty = party;
+ fallbackPhoneNumber = phoneNumber;
+
+ return true; // Preferred address is found
}
- // 2a. This is one of many other addresses to be checked
+ // 2a. This is one of many other addresses to be checked (e-mail has priority)
if (fallbackEmailAddress.IsNullOrEmpty() && // Only the first encountered one matters
emailAddress.IsNotNullOrEmpty())
{
fallbackEmailAddress = emailAddress;
- fallbackEmailOwningParty = partyResult;
+ fallbackEmailOwningParty = party;
continue; // The e-mail address always has priority over the phone number.
// If any e-mail address was found during this run then the phone
// number doesn't matter anymore since it will not be returned anyway
}
+ // 2b. This address is not preferred but could be the only which was found as matching
if (fallbackPhoneNumber.IsNullOrEmpty() && // Only the first encountered one matters
phoneNumber.IsNotNullOrEmpty())
{
fallbackPhoneNumber = phoneNumber;
- fallbackPhoneOwningParty = partyResult;
+ fallbackPhoneOwningParty = party;
}
}
- // 2b. FALLBACK APPROACH: If the party's preferred address couldn't be determined
+ return false;
+ }
+
+ // NOTE: Checks alternative contact addresses
+ private static (PartyResult, DistributionChannels, string EmailAddress, string PhoneNumber) GetMatchingContactDetails(
+ PartyResult fallbackEmailOwningParty, PartyResult fallbackPhoneOwningParty,
+ string fallbackEmailAddress, string fallbackPhoneNumber)
+ {
+ // 3a. FALLBACK APPROACH: If the party's preferred address couldn't be determined
// the email address has priority and the first encountered one should be returned
if (fallbackEmailAddress.IsNotNullOrEmpty())
{
@@ -213,7 +200,7 @@ internal static (PartyResult, DistributionChannels, string EmailAddress, string
EmailAddress: fallbackEmailAddress, PhoneNumber: string.Empty);
}
- // 2c. FALLBACK APPROACH: If the email also couldn't be determined then alternatively
+ // 3b. FALLBACK APPROACH: If the email also couldn't be determined then alternatively
// the first encountered telephone number (for SMS) should be returned instead
if (fallbackPhoneNumber.IsNotNullOrEmpty())
{
@@ -221,12 +208,11 @@ internal static (PartyResult, DistributionChannels, string EmailAddress, string
EmailAddress: string.Empty, PhoneNumber: fallbackPhoneNumber);
}
- // 2d. In the case of worst possible scenario, that preferred address couldn't be determined
+ // 3c. In the case of worst possible scenario, that preferred address couldn't be determined
// neither any existing email address nor telephone number, then process can't be finished
throw new HttpRequestException(Resources.HttpRequest_ERROR_NoDigitalAddresses);
}
- #region Helper methods
///
/// Checks if the value from generic JSON property "Type" is
/// matching to the predefined names of digital address types.
diff --git a/EventsHandler/Api/EventsHandler/Mapping/Models/POCOs/OpenZaak/CaseRole.cs b/EventsHandler/Api/EventsHandler/Mapping/Models/POCOs/OpenZaak/CaseRole.cs
index 7884982e..129ca475 100644
--- a/EventsHandler/Api/EventsHandler/Mapping/Models/POCOs/OpenZaak/CaseRole.cs
+++ b/EventsHandler/Api/EventsHandler/Mapping/Models/POCOs/OpenZaak/CaseRole.cs
@@ -19,7 +19,7 @@ public struct CaseRole : IJsonSerializable
[JsonInclude]
[JsonPropertyName("betrokkene")] // ENG: Involved party
[JsonPropertyOrder(0)]
- public Uri InvolvedPartyUri { get; internal set; } = DefaultValues.Models.EmptyUri;
+ public Uri InvolvedPartyUri { get; internal set; } = DefaultValues.Models.EmptyUri; // NOTE: Might be missing for Citizens
///
/// The general description of the which includes the "initiator role" of the .
@@ -33,10 +33,10 @@ public struct CaseRole : IJsonSerializable
///
/// The data subject identification which includes details about a single citizen related to this .
///
- [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
+ [JsonInclude]
[JsonPropertyName("betrokkeneIdentificatie")] // ENG: Data subject (party) identification
[JsonPropertyOrder(2)]
- public PartyData Party { get; internal set; }
+ public PartyData? Party { get; internal set; } = new PartyData(); // NOTE: Might be missing for Organizations
///
/// Initializes a new instance of the struct.
diff --git a/EventsHandler/Api/EventsHandler/Mapping/Models/POCOs/OpenZaak/v1/CaseRoles.cs b/EventsHandler/Api/EventsHandler/Mapping/Models/POCOs/OpenZaak/v1/CaseRoles.cs
index d053d9e2..7fd290d2 100644
--- a/EventsHandler/Api/EventsHandler/Mapping/Models/POCOs/OpenZaak/v1/CaseRoles.cs
+++ b/EventsHandler/Api/EventsHandler/Mapping/Models/POCOs/OpenZaak/v1/CaseRoles.cs
@@ -2,6 +2,7 @@
using EventsHandler.Mapping.Models.Interfaces;
using EventsHandler.Properties;
+using EventsHandler.Services.Settings.Configuration;
using Microsoft.IdentityModel.Tokens;
using System.Text.Json.Serialization;
@@ -43,23 +44,30 @@ public CaseRoles()
}
///
- /// Gets the most recent (last) .
+ /// Gets the desired .
///
///
/// The single role.
///
- /// // TODO: KeyNotFoundException
- internal readonly CaseRole CaseRole
+ ///
+ ///
+ internal readonly CaseRole CaseRole(WebApiConfiguration configuration)
{
- get
+ if (this.Results.IsNullOrEmpty())
{
- if (this.Results.IsNullOrEmpty())
+ throw new HttpRequestException(Resources.HttpRequest_ERROR_EmptyCaseRoles);
+ }
+
+ foreach (CaseRole caseRole in this.Results)
+ {
+ if (caseRole.InitiatorRole == configuration.AppSettings.Variables.InitiatorRole())
{
- throw new HttpRequestException(Resources.HttpRequest_ERROR_EmptyCaseRoles);
+ return caseRole;
}
-
- return this.Results[^1]; // TODO: Get party by initiator role
}
+
+ // Zero initiator results were found (there is no initiator)
+ throw new HttpRequestException(Resources.HttpRequest_ERROR_MissingInitiatorRole);
}
}
}
\ No newline at end of file
diff --git a/EventsHandler/Api/EventsHandler/Mapping/Models/POCOs/OpenZaak/v2/CaseRoles.cs b/EventsHandler/Api/EventsHandler/Mapping/Models/POCOs/OpenZaak/v2/CaseRoles.cs
index c065cb15..14515572 100644
--- a/EventsHandler/Api/EventsHandler/Mapping/Models/POCOs/OpenZaak/v2/CaseRoles.cs
+++ b/EventsHandler/Api/EventsHandler/Mapping/Models/POCOs/OpenZaak/v2/CaseRoles.cs
@@ -44,7 +44,7 @@ public CaseRoles()
}
///
- /// Gets the with matching "initiator role" set.
+ /// Gets the desired .
///
///
/// The single role.
@@ -53,7 +53,6 @@ public CaseRoles()
///
internal readonly CaseRole CaseRole(WebApiConfiguration configuration)
{
- // Response does not contain any results (check notification or project configuration)
if (this.Results.IsNullOrEmpty())
{
throw new HttpRequestException(Resources.HttpRequest_ERROR_EmptyCaseRoles);
diff --git a/EventsHandler/Api/EventsHandler/Properties/Resources.Designer.cs b/EventsHandler/Api/EventsHandler/Properties/Resources.Designer.cs
index 6e39b8e1..82af6c12 100644
--- a/EventsHandler/Api/EventsHandler/Properties/Resources.Designer.cs
+++ b/EventsHandler/Api/EventsHandler/Properties/Resources.Designer.cs
@@ -205,7 +205,7 @@ internal static string Configuration_ERROR_VersionTelemetryUnknown {
}
///
- /// Looks up a localized string similar to The given value cannot be deserialized into dedicated target object.
+ /// Looks up a localized string similar to The given JSON cannot be deserialized | Target model: {0} | Failed: {1} | Reason: {2} | All required properties: {3} | Source JSON: {4}.
///
internal static string Deserialization_ERROR_CannotDeserialize_Message {
get {
@@ -214,29 +214,11 @@ internal static string Deserialization_ERROR_CannotDeserialize_Message {
}
///
- /// Looks up a localized string similar to Required properties.
+ /// Looks up a localized string similar to The following properties were missing in JSON but they are required..
///
- internal static string Deserialization_ERROR_CannotDeserialize_Required {
+ internal static string Deserialization_ERROR_CannotDeserialize_RequiredProperties {
get {
- return ResourceManager.GetString("Deserialization_ERROR_CannotDeserialize_Required", resourceCulture);
- }
- }
-
- ///
- /// Looks up a localized string similar to Target.
- ///
- internal static string Deserialization_ERROR_CannotDeserialize_Target {
- get {
- return ResourceManager.GetString("Deserialization_ERROR_CannotDeserialize_Target", resourceCulture);
- }
- }
-
- ///
- /// Looks up a localized string similar to Value.
- ///
- internal static string Deserialization_ERROR_CannotDeserialize_Value {
- get {
- return ResourceManager.GetString("Deserialization_ERROR_CannotDeserialize_Value", resourceCulture);
+ return ResourceManager.GetString("Deserialization_ERROR_CannotDeserialize_RequiredProperties", resourceCulture);
}
}
@@ -618,15 +600,6 @@ internal static string HttpRequest_ERROR_NoLastStatus {
}
}
- ///
- /// Looks up a localized string similar to HTTP Request: The main object could not be retrieved from OpenZaak Web API service..
- ///
- internal static string HttpRequest_ERROR_NoMainObject {
- get {
- return ResourceManager.GetString("HttpRequest_ERROR_NoMainObject", resourceCulture);
- }
- }
-
///
/// Looks up a localized string similar to HTTP Request: The message (object) could not be retrieved from Objecten Web API service..
///
@@ -862,11 +835,11 @@ internal static string Processing_ABORT_DoNotSendNotification_TaskClosed {
}
///
- /// Looks up a localized string similar to The notification can not be sent if the task is not assigned to a person (missing BSN number)..
+ /// Looks up a localized string similar to The notification can not be sent if the task is not assigned to a person (missing BSN number) or to an organization (missing KVK number)..
///
- internal static string Processing_ABORT_DoNotSendNotification_TaskNotPerson {
+ internal static string Processing_ABORT_DoNotSendNotification_TaskIdTypeNotSupported {
get {
- return ResourceManager.GetString("Processing_ABORT_DoNotSendNotification_TaskNotPerson", resourceCulture);
+ return ResourceManager.GetString("Processing_ABORT_DoNotSendNotification_TaskIdTypeNotSupported", resourceCulture);
}
}
@@ -1032,15 +1005,6 @@ internal static string Register_NotifyNL_SUCCESS_NotificationSent {
}
}
- ///
- /// Looks up a localized string similar to .
- ///
- internal static string String1 {
- get {
- return ResourceManager.GetString("String1", resourceCulture);
- }
- }
-
///
/// Looks up a localized string similar to Insert received JWT token here.
///
diff --git a/EventsHandler/Api/EventsHandler/Properties/Resources.resx b/EventsHandler/Api/EventsHandler/Properties/Resources.resx
index 8c58f205..ac58b542 100644
--- a/EventsHandler/Api/EventsHandler/Properties/Resources.resx
+++ b/EventsHandler/Api/EventsHandler/Properties/Resources.resx
@@ -328,13 +328,8 @@
OMC
- The given value cannot be deserialized into dedicated target object
-
-
- Target
-
-
- Value
+ The given JSON cannot be deserialized | Target model: {0} | Failed: {1} | Reason: {2} | All required properties: {3} | Source JSON: {4}
+ {0} = The model used as target for deserialzation, {1} = The properties that failed during deserialization, {2} = The reason why deserialization failed, {3} = The properties that are required, {4} = The JSON used as data to be deserialized
HTTP Request: The case (obtained from OpenZaak Web API service) does not contain any statuses.
@@ -379,9 +374,6 @@
SENDER: In the received notification some [Required] properties are either null, empty, or with default values.
-
- HTTP Request: The main object could not be retrieved from OpenZaak Web API service.
-
The notification can not be sent because the case type with identification '{0}' is not included in the {1}.
{0} = Case Type ID, {1} = Name of the environment variable holding this value
@@ -398,8 +390,8 @@
The notification can not be sent if the task is already closed.
-
- The notification can not be sent if the task is not assigned to a person (missing BSN number).
+
+ The notification can not be sent if the task is not assigned to a person (missing BSN number) or to an organization (missing KVK number).
Internal error: The given URI is not a case type.
@@ -474,9 +466,6 @@
Internal error: The given URI is not an object.
HTTP Status Code = 422. The developer used wrong method or in a wrong way
-
- Required properties
-
The notification can not be sent for this object type GUID: {0}. Check respective whitelist (depends on type of your object): {1}.
{0} = Object Type ID, {1} = Generic name of environment variables holding the whitelisted values
@@ -488,9 +477,6 @@
The version of Besluiten service to be used by OMC is unknown or not supported. Check used version of OMC workflow.
-
-
-
HTTP Request: Party results could not be retrieved from OpenKlant Web API service.
@@ -502,4 +488,7 @@
Internal error: The given URI is not a party.
HTTP Status Code = 422. The developer used wrong method or in a wrong way
+
+ The following properties were missing in JSON but they are required.
+
\ No newline at end of file
diff --git a/EventsHandler/Api/EventsHandler/Properties/launchSettings.json b/EventsHandler/Api/EventsHandler/Properties/launchSettings.json
index 4eba69f5..4eaf33ea 100644
--- a/EventsHandler/Api/EventsHandler/Properties/launchSettings.json
+++ b/EventsHandler/Api/EventsHandler/Properties/launchSettings.json
@@ -83,7 +83,7 @@
"USER_WHITELIST_MESSAGEOBJECTTYPE_UUID": "38327774-7023-4f25-9386-acb0c6f10636",
"USER_WHITELIST_DECISIONINFOOBJECTTYPE_UUIDS": "f482b7a3-22b7-40e2-a187-52f737d4ef44",
- "USER_VARIABLES_OBJECTEN_MESSAGEOBJECTTYPE_VERSION": "1",
+ "USER_VARIABLES_OBJECTEN_MESSAGEOBJECTTYPE_VERSION": "2",
"SENTRY_DSN": "https://1db70f552fb2bdcab8571661a3db6d70@o4507152178741248.ingest.de.sentry.io/4507152289431632",
"SENTRY_ENVIRONMENT": "Worth Production | Workflow v1"
@@ -152,7 +152,7 @@
"USER_WHITELIST_MESSAGEOBJECTTYPE_UUID": "38327774-7023-4f25-9386-acb0c6f10636",
"USER_WHITELIST_DECISIONINFOOBJECTTYPE_UUIDS": "f482b7a3-22b7-40e2-a187-52f737d4ef44",
- "USER_VARIABLES_OBJECTEN_MESSAGEOBJECTTYPE_VERSION": "1",
+ "USER_VARIABLES_OBJECTEN_MESSAGEOBJECTTYPE_VERSION": "2",
"SENTRY_DSN": "https://1db70f552fb2bdcab8571661a3db6d70@o4507152178741248.ingest.de.sentry.io/4507152289431632",
"SENTRY_ENVIRONMENT": "Worth Development | Workflow v2"
diff --git a/EventsHandler/Api/EventsHandler/Services/DataProcessing/Strategy/Implementations/DecisionMadeScenario.cs b/EventsHandler/Api/EventsHandler/Services/DataProcessing/Strategy/Implementations/DecisionMadeScenario.cs
index 9e580abf..d3364c7f 100644
--- a/EventsHandler/Api/EventsHandler/Services/DataProcessing/Strategy/Implementations/DecisionMadeScenario.cs
+++ b/EventsHandler/Api/EventsHandler/Services/DataProcessing/Strategy/Implementations/DecisionMadeScenario.cs
@@ -89,7 +89,7 @@ protected override async Task PrepareDataAsync(NotificationEvent n
await this._queryContext.GetCaseStatusesAsync( // 2. Case statuses
this._decision.CaseUri)); // 1. Case URI
- // Validation #4: The case identifier must be whitelisted
+ // Validation #4: The case type identifier must be whitelisted
ValidateCaseId(
this.Configuration.User.Whitelist.DecisionMade_IDs().IsAllowed,
this._caseType.Identification, GetWhitelistEnvVarName());
@@ -277,11 +277,13 @@ private async Task GetValidInfoObjectUrisAsync(IQueryContext queryContex
///
private string PrepareDataJson(string subject, string body, string commaSeparatedUris)
{
+ const string missing = "-";
+
string identificationType = (this._bsnNumber.IsNullOrEmpty() ? IdTypes.Unknown : IdTypes.Bsn).GetEnumName(); // TODO: Which type should be used for organization?
string identificationValue = this._bsnNumber.IsNullOrEmpty() ? "-" : this._bsnNumber; // TODO: Which value should be used for organization?
- return $"\"onderwerp\":\"{subject}\"," +
- $"\"berichtTekst\":\"{body}\"," +
+ return $"\"onderwerp\":\"{(subject.IsNullOrEmpty() ? missing : subject)}\"," +
+ $"\"berichtTekst\":\"{(body.IsNullOrEmpty() ? missing : body)}\"," +
$"\"publicatiedatum\":\"{this._decision.PublicationDate:O}\"," +
$"\"referentie\":\"{this._decisionResource.DecisionUri}\"," +
$"\"handelingsperspectief\":\"informatie verstrekken\"," +
diff --git a/EventsHandler/Api/EventsHandler/Services/DataProcessing/Strategy/Implementations/TaskAssignedScenario.cs b/EventsHandler/Api/EventsHandler/Services/DataProcessing/Strategy/Implementations/TaskAssignedScenario.cs
index 44a7acb6..257cc445 100644
--- a/EventsHandler/Api/EventsHandler/Services/DataProcessing/Strategy/Implementations/TaskAssignedScenario.cs
+++ b/EventsHandler/Api/EventsHandler/Services/DataProcessing/Strategy/Implementations/TaskAssignedScenario.cs
@@ -57,12 +57,10 @@ protected override async Task PrepareDataAsync(NotificationEvent n
}
// Validation #2: The task needs to be assigned to a person
- // TODO: Disabled this check and its corresponding test because this is no longer correct
- // TODO: We should expand this check with a check on the involvedPartyUri being correct
- //if (this._taskData.Identification.Type != IdTypes.Bsn)
- //{
- // throw new AbortedNotifyingException(Resources.Processing_ABORT_DoNotSendNotification_TaskNotPerson);
- //}
+ if (this._taskData.Identification.Type is not (IdTypes.Bsn or IdTypes.Kvk))
+ {
+ throw new AbortedNotifyingException(Resources.Processing_ABORT_DoNotSendNotification_TaskIdTypeNotSupported);
+ }
CaseType caseType = await this._queryContext.GetLastCaseTypeAsync( // 3. Case type
await this._queryContext.GetCaseStatusesAsync( // 2. Case statuses
@@ -79,10 +77,14 @@ await this._queryContext.GetCaseStatusesAsync( // 2. Case statuses
this._case = await this._queryContext.GetCaseAsync(this._taskData.CaseUri);
// Preparing party details
+ string? bsnNumber = this._taskData.Identification.Type == IdTypes.Bsn
+ ? this._taskData.Identification.Value // BSN number
+ : null;
+
return new PreparedData(
party: await this._queryContext.GetPartyDataAsync(
caseUri: this._case.Uri,
- bsnNumber: this._taskData.Identification.Value), // BSN number
+ bsnNumber),
caseUri: this._case.Uri);
}
#endregion
diff --git a/EventsHandler/Api/EventsHandler/Services/DataQuerying/Composition/Strategy/Besluiten/v1/QueryBesluiten.cs b/EventsHandler/Api/EventsHandler/Services/DataQuerying/Composition/Strategy/Besluiten/v1/QueryBesluiten.cs
index 3db5a67e..5179521a 100644
--- a/EventsHandler/Api/EventsHandler/Services/DataQuerying/Composition/Strategy/Besluiten/v1/QueryBesluiten.cs
+++ b/EventsHandler/Api/EventsHandler/Services/DataQuerying/Composition/Strategy/Besluiten/v1/QueryBesluiten.cs
@@ -8,7 +8,7 @@ namespace EventsHandler.Services.DataQuerying.Composition.Strategy.Besluiten.v1
{
///
///
- /// Version: "Besluiten" (v1+) Web API service | "OMC workflow" v1.
+ /// Version: "Besluiten" (v1) Web API service | "OMC workflow" v1.
///
///
internal sealed class QueryBesluiten : IQueryBesluiten
diff --git a/EventsHandler/Api/EventsHandler/Services/DataQuerying/Composition/Strategy/ObjectTypen/v1/QueryObjectTypen.cs b/EventsHandler/Api/EventsHandler/Services/DataQuerying/Composition/Strategy/ObjectTypen/v1/QueryObjectTypen.cs
index 638c860d..106f68fd 100644
--- a/EventsHandler/Api/EventsHandler/Services/DataQuerying/Composition/Strategy/ObjectTypen/v1/QueryObjectTypen.cs
+++ b/EventsHandler/Api/EventsHandler/Services/DataQuerying/Composition/Strategy/ObjectTypen/v1/QueryObjectTypen.cs
@@ -8,7 +8,7 @@ namespace EventsHandler.Services.DataQuerying.Composition.Strategy.ObjectTypen.v
{
///
///
- /// Version: "ObjectTypen" (v2+) Web API service | "OMC workflow" v1.
+ /// Version: "ObjectTypen" (v1) Web API service | "OMC workflow" v1.
///
///
internal sealed class QueryObjectTypen : IQueryObjectTypen
diff --git a/EventsHandler/Api/EventsHandler/Services/DataQuerying/Composition/Strategy/Objecten/v1/QueryObjecten.cs b/EventsHandler/Api/EventsHandler/Services/DataQuerying/Composition/Strategy/Objecten/v1/QueryObjecten.cs
index a9382c24..3c641ba8 100644
--- a/EventsHandler/Api/EventsHandler/Services/DataQuerying/Composition/Strategy/Objecten/v1/QueryObjecten.cs
+++ b/EventsHandler/Api/EventsHandler/Services/DataQuerying/Composition/Strategy/Objecten/v1/QueryObjecten.cs
@@ -8,7 +8,7 @@ namespace EventsHandler.Services.DataQuerying.Composition.Strategy.Objecten.v1
{
///
///
- /// Version: "Objecten" (v2+) Web API service | "OMC workflow" v1.
+ /// Version: "Objecten" (v1) Web API service | "OMC workflow" v1.
///
///
internal sealed class QueryObjecten : IQueryObjecten
diff --git a/EventsHandler/Api/EventsHandler/Services/DataQuerying/Composition/Strategy/OpenKlant/v1/QueryKlant.cs b/EventsHandler/Api/EventsHandler/Services/DataQuerying/Composition/Strategy/OpenKlant/v1/QueryKlant.cs
index 0502e19f..1eefe41b 100644
--- a/EventsHandler/Api/EventsHandler/Services/DataQuerying/Composition/Strategy/OpenKlant/v1/QueryKlant.cs
+++ b/EventsHandler/Api/EventsHandler/Services/DataQuerying/Composition/Strategy/OpenKlant/v1/QueryKlant.cs
@@ -17,7 +17,7 @@ namespace EventsHandler.Services.DataQuerying.Composition.Strategy.OpenKlant.v1
{
///
///
- /// Version: "OpenKlant" (v1+) Web API service | "OMC workflow" v1.
+ /// Version: "OpenKlant" (v1) Web API service | "OMC workflow" v1.
///
///
internal sealed class QueryKlant : IQueryKlant
diff --git a/EventsHandler/Api/EventsHandler/Services/DataQuerying/Composition/Strategy/OpenKlant/v2/QueryKlant.cs b/EventsHandler/Api/EventsHandler/Services/DataQuerying/Composition/Strategy/OpenKlant/v2/QueryKlant.cs
index 45708d53..dff46bd0 100644
--- a/EventsHandler/Api/EventsHandler/Services/DataQuerying/Composition/Strategy/OpenKlant/v2/QueryKlant.cs
+++ b/EventsHandler/Api/EventsHandler/Services/DataQuerying/Composition/Strategy/OpenKlant/v2/QueryKlant.cs
@@ -17,7 +17,7 @@ namespace EventsHandler.Services.DataQuerying.Composition.Strategy.OpenKlant.v2
{
///
///
- /// Version: "OpenKlant" (v2+) Web API service | "OMC workflow" v2.
+ /// Version: "OpenKlant" (v2) Web API service | "OMC workflow" v2.
///
///
internal sealed class QueryKlant : IQueryKlant
diff --git a/EventsHandler/Api/EventsHandler/Services/DataQuerying/Composition/Strategy/OpenZaak/Interfaces/IQueryZaak.cs b/EventsHandler/Api/EventsHandler/Services/DataQuerying/Composition/Strategy/OpenZaak/Interfaces/IQueryZaak.cs
index b68f4e25..be564188 100644
--- a/EventsHandler/Api/EventsHandler/Services/DataQuerying/Composition/Strategy/OpenZaak/Interfaces/IQueryZaak.cs
+++ b/EventsHandler/Api/EventsHandler/Services/DataQuerying/Composition/Strategy/OpenZaak/Interfaces/IQueryZaak.cs
@@ -123,8 +123,8 @@ internal async Task GetBsnNumberAsync(IQueryBase queryBase, Uri caseUri)
}
return (await GetCaseRoleAsync(queryBase, caseUri))
- .Party
- .BsnNumber;
+ .Party?
+ .BsnNumber ?? string.Empty;
}
#endregion
diff --git a/EventsHandler/Api/EventsHandler/Services/DataQuerying/Composition/Strategy/OpenZaak/v1/QueryZaak.cs b/EventsHandler/Api/EventsHandler/Services/DataQuerying/Composition/Strategy/OpenZaak/v1/QueryZaak.cs
index 6c8724d2..d4d0615d 100644
--- a/EventsHandler/Api/EventsHandler/Services/DataQuerying/Composition/Strategy/OpenZaak/v1/QueryZaak.cs
+++ b/EventsHandler/Api/EventsHandler/Services/DataQuerying/Composition/Strategy/OpenZaak/v1/QueryZaak.cs
@@ -13,7 +13,7 @@ namespace EventsHandler.Services.DataQuerying.Composition.Strategy.OpenZaak.v1
{
///
///
- /// Version: "OpenZaak" (v1+) Web API service | "OMC workflow" v1.
+ /// Version: "OpenZaak" (v1) Web API service | "OMC workflow" v1.
///
///
internal sealed class QueryZaak : IQueryZaak
@@ -39,7 +39,7 @@ async Task IQueryZaak.GetCaseRoleAsync(IQueryBase queryBase, Uri caseU
const string subjectType = "natuurlijk_persoon"; // NOTE: Only this specific parameter value is supported
return (await GetCaseRolesV1Async(queryBase, caseUri, subjectType))
- .CaseRole;
+ .CaseRole(((IQueryZaak)this).Configuration);
}
private async Task GetCaseRolesV1Async(IQueryBase queryBase, Uri caseUri, string subjectType)
diff --git a/EventsHandler/Api/EventsHandler/Services/DataQuerying/Composition/Strategy/OpenZaak/v2/QueryZaak.cs b/EventsHandler/Api/EventsHandler/Services/DataQuerying/Composition/Strategy/OpenZaak/v2/QueryZaak.cs
index ca04149c..5dc014dd 100644
--- a/EventsHandler/Api/EventsHandler/Services/DataQuerying/Composition/Strategy/OpenZaak/v2/QueryZaak.cs
+++ b/EventsHandler/Api/EventsHandler/Services/DataQuerying/Composition/Strategy/OpenZaak/v2/QueryZaak.cs
@@ -13,7 +13,7 @@ namespace EventsHandler.Services.DataQuerying.Composition.Strategy.OpenZaak.v2
{
///
///
- /// Version: "OpenZaak" (v1+) Web API service | "OMC workflow" v2.
+ /// Version: "OpenZaak" (v2) Web API service | "OMC workflow" v2.
///
///
internal sealed class QueryZaak : IQueryZaak
@@ -42,7 +42,7 @@ async Task IQueryZaak.GetCaseRoleAsync(IQueryBase queryBase, Uri caseU
.CaseRole(((IQueryZaak)this).Configuration);
}
- private async Task GetCaseRolesV2Async(IQueryBase queryBase, Uri caseUri, string subjectType)
+ private async Task GetCaseRolesV2Async(IQueryBase queryBase, Uri caseUri, string subjectType) // TODO: Not used yet (at the moment, consulting)
{
// Predefined URL components
string rolesEndpoint = $"https://{((IQueryZaak)this).GetDomain()}/rollen";
diff --git a/EventsHandler/Api/EventsHandler/Services/Serialization/SpecificSerializer.cs b/EventsHandler/Api/EventsHandler/Services/Serialization/SpecificSerializer.cs
index a40b7d2f..1092e578 100644
--- a/EventsHandler/Api/EventsHandler/Services/Serialization/SpecificSerializer.cs
+++ b/EventsHandler/Api/EventsHandler/Services/Serialization/SpecificSerializer.cs
@@ -17,11 +17,13 @@ namespace EventsHandler.Services.Serialization
internal sealed class SpecificSerializer : ISerializationService
{
private static readonly ConcurrentDictionary s_cachedRequiredProperties = new();
+
+ #region Custom converters
private static readonly JsonSerializerOptions s_serializerOptions = new()
{
PropertyNameCaseInsensitive = true,
-
- // Global converters
+
+ // They will be applied globally, whenever JSON Serializer Options are used
Converters =
{
new BoolJsonConverter(),
@@ -34,6 +36,7 @@ internal sealed class SpecificSerializer : ISerializationService
new UriJsonConverter()
}
};
+ #endregion
///
TModel ISerializationService.Deserialize(object json)
@@ -42,15 +45,9 @@ TModel ISerializationService.Deserialize(object json)
{
return JsonSerializer.Deserialize($"{json}", s_serializerOptions);
}
- catch (JsonException)
+ catch (JsonException exception)
{
- string requiredProperties = GetRequiredMembers();
-
- throw new JsonException(message:
- $"{Resources.Deserialization_ERROR_CannotDeserialize_Message} | " +
- $"{Resources.Deserialization_ERROR_CannotDeserialize_Target}: {typeof(TModel).Name} | " +
- $"{Resources.Deserialization_ERROR_CannotDeserialize_Value}: {json} | " +
- $"{Resources.Deserialization_ERROR_CannotDeserialize_Required}: {(requiredProperties.IsNullOrEmpty() ? "_" : requiredProperties)}");
+ return GetDeserializationException(exception, json);
}
}
@@ -61,6 +58,39 @@ string ISerializationService.Serialize(TModel model)
}
#region Helper methods
+ private const string EmptyRequired = "_";
+ private const string QuotationMark = "'";
+
+ ///
+ /// Gets the human-friendly readable exception with details why the process of deserialization failed.
+ ///
+ private static TModel GetDeserializationException(JsonException exception, object json)
+ where TModel : struct, IJsonSerializable
+ {
+ string failed;
+ string reason;
+
+ if (exception.Path.IsNotNullOrEmpty() &&
+ exception.InnerException != null)
+ {
+ failed = $"{QuotationMark}{exception.Path}{QuotationMark}";
+ reason = exception.InnerException.Message;
+ }
+ else
+ {
+ failed = $"{QuotationMark}{exception.Message[(exception.Message.IndexOf(':') + 2)..]}{QuotationMark}";
+ reason = Resources.Deserialization_ERROR_CannotDeserialize_RequiredProperties;
+ }
+
+ throw new JsonException(message:
+ string.Format(Resources.Deserialization_ERROR_CannotDeserialize_Message,
+ /* {0} - Target */ $"{QuotationMark}{typeof(TModel).Name}.cs{QuotationMark}",
+ /* {1} - Failed */ failed,
+ /* {2} - Reason */ reason,
+ /* {3} - Required */ GetRequiredMembers(),
+ /* {4} - JSON */ json));
+ }
+
///
/// Gets text representation of this specific object.
///
@@ -77,11 +107,11 @@ private static string GetRequiredMembers()
}
catch (Exception)
{
- return string.Empty;
+ return EmptyRequired;
}
}
- private static IEnumerable GetRequiredPropertiesNames(IReflect type, string parentName = "")
+ private static IEnumerable GetRequiredPropertiesNames(IReflect type, string parentName = QuotationMark)
{
IEnumerable requiredProperties = type
.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
@@ -97,7 +127,14 @@ private static IEnumerable GetRequiredPropertiesNames(IReflect type, str
yield return GetRequiredPropertiesNames(requiredProperty.PropertyType, AppendParentName(parentName, requiredProperty))
.Join();
}
- // Case #2: Collection type
+ // Case #2: Array
+ else if (typeof(IEnumerable).IsAssignableFrom(requiredProperty.PropertyType.BaseType))
+ {
+ // Get properties of the array element type
+ yield return GetRequiredPropertiesNames(requiredProperty.PropertyType.GetElementType()!, AppendParentName(parentName, requiredProperty))
+ .Join();
+ }
+ // Case #3: Generic collection
else if (requiredProperty.PropertyType.IsGenericType && // Prevent exceptions if the simple type is encountered
typeof(IEnumerable).IsAssignableFrom(requiredProperty.PropertyType.GetGenericTypeDefinition()))
{
@@ -105,10 +142,10 @@ private static IEnumerable GetRequiredPropertiesNames(IReflect type, str
yield return GetRequiredPropertiesNames(requiredProperty.PropertyType.GenericTypeArguments[0], AppendParentName(parentName, requiredProperty))
.Join();
}
- // Case #3: Simple type
+ // Case #4: Simple type
else
{
- yield return IncludeParentName(parentName, requiredProperty);
+ yield return $"{IncludeParentName(parentName, requiredProperty)}{QuotationMark}";
}
}
}
diff --git a/EventsHandler/Tests/UnitTests/EventsHandler.Tests/Mapping/Models/POCOs/OpenKlant/v2/PartyResultsTests.cs b/EventsHandler/Tests/UnitTests/EventsHandler.Tests/Mapping/Models/POCOs/OpenKlant/v2/PartyResultsTests.cs
index e3a163cb..77546e93 100644
--- a/EventsHandler/Tests/UnitTests/EventsHandler.Tests/Mapping/Models/POCOs/OpenKlant/v2/PartyResultsTests.cs
+++ b/EventsHandler/Tests/UnitTests/EventsHandler.Tests/Mapping/Models/POCOs/OpenKlant/v2/PartyResultsTests.cs
@@ -28,9 +28,9 @@ public void TestsCleanup()
this._validAppSettingsConfiguration.Dispose();
}
- #region Party (method)
+ #region Party (processing multiple roles)
[Test]
- public void Party_Method_ForMissingResults_ThrowsHttpRequestException()
+ public void Party_ForMany_MissingResults_ThrowsHttpRequestException()
{
// Arrange
var partyResults = new PartyResults(); // Empty "Results" inside
@@ -40,7 +40,7 @@ public void Party_Method_ForMissingResults_ThrowsHttpRequestException()
}
[Test]
- public void Party_Method_ForExistingResults_ButMissingAddresses_ThrowsHttpRequestException()
+ public void Party_ForMany_ExistingResults_ButMissingAddresses_ThrowsHttpRequestException()
{
// Arrange
PartyResults partyResults = GetTestPartyResults(); // Empty "DigitalAddresses" inside
@@ -50,48 +50,10 @@ public void Party_Method_ForExistingResults_ButMissingAddresses_ThrowsHttpReques
}
[Test]
- public void Party_Method_ForExistingResults_ButEmptyDigitalAddresses_ThrowsHttpRequestException()
+ public void Party_ForMany_ExistingResults_ButEmptyDigitalAddresses_ThrowsHttpRequestException()
{
// Arrange
- var partyId = Guid.NewGuid();
- Guid firstAddressId = GetUniqueId(partyId);
- Guid secondAddressId = GetUniqueId(firstAddressId);
-
- var partyResult = new PartyResult
- {
- PreferredDigitalAddress = new DigitalAddressShort
- {
- Id = partyId
- },
- Identification = new PartyIdentification
- {
- Details = new PartyDetails
- {
- Name = "Samantha",
- SurnamePrefix = string.Empty,
- Surname = "Rogers"
- }
- },
- Expansion = new Expansion
- {
- DigitalAddresses = new List
- {
- new() // Just empty address
- {
- Id = firstAddressId,
- Value = string.Empty,
- Type = this._validAppSettingsConfiguration.AppSettings.Variables.PhoneGenericDescription()
- },
- new() // Just empty address
- {
- Id = secondAddressId,
- Value = string.Empty,
- Type = this._validAppSettingsConfiguration.AppSettings.Variables.EmailGenericDescription()
- }
- }
- }
- };
-
+ PartyResult partyResult = GetTestPartyResult_None(this._validAppSettingsConfiguration);
PartyResults partyResults = GetTestPartyResults(partyResult); // Missing e-mails and phone numbers
// Act & Assert
@@ -99,14 +61,14 @@ public void Party_Method_ForExistingResults_ButEmptyDigitalAddresses_ThrowsHttpR
}
[Test]
- public void Party_Method_ForExistingResults_With_MatchingPreferredAddress_ReturnsExpectedResult_Email()
+ public void Party_ForMany_ExistingResults_With_MatchingPreferredAddress_ReturnsExpectedResult_Email()
{
// Arrange
var testId = Guid.NewGuid();
- PartyResult testPartyEmail = GetTestPartyResult_Email(this._validAppSettingsConfiguration, testId, testId);
PartyResult testPartyPhone = GetTestPartyResult_Phone(this._validAppSettingsConfiguration, testId, testId);
- PartyResults testPartyResults = GetTestPartyResults(testPartyEmail, testPartyPhone);
+ PartyResult testPartyEmail = GetTestPartyResult_Email(this._validAppSettingsConfiguration, testId, testId); // Email should have priority over phone
+ PartyResults testPartyResults = GetTestPartyResults(testPartyEmail, testPartyPhone); // If both phone and email are preferred, email would have priority
// Act
(PartyResult actualParty, DistributionChannels actualDistChannel, string actualEmailAddress, string actualPhoneNumber)
@@ -117,18 +79,18 @@ public void Party_Method_ForExistingResults_With_MatchingPreferredAddress_Return
{
Assert.That(actualParty, Is.EqualTo(testPartyEmail));
Assert.That(actualDistChannel, Is.EqualTo(DistributionChannels.Email));
- Assert.That(actualEmailAddress, Is.EqualTo($"first_{TestEmail}"));
+ Assert.That(actualEmailAddress, Is.EqualTo($"second_{TestEmail}")); // The preferred address was found
Assert.That(actualPhoneNumber, Is.Empty); // Since email is priority, phone should be ignored
});
}
[Test]
- public void Party_Method_ForExistingResults_Without_MatchingPreferredAddress_ReturnsExpectedResult_Email_FirstEncountered_PriorityOverPhone()
+ public void Party_ForMany_ExistingResults_Without_MatchingPreferredAddress_ReturnsExpectedResult_Email_FirstEncountered_PriorityOverPhone()
{
// Arrange
- PartyResult testPartyEmail = GetTestPartyResult_Email(this._validAppSettingsConfiguration, Guid.Empty, Guid.NewGuid());
PartyResult testPartyPhone = GetTestPartyResult_Phone(this._validAppSettingsConfiguration, Guid.Empty, Guid.NewGuid());
- PartyResults testPartyResults = GetTestPartyResults(testPartyEmail, testPartyPhone);
+ PartyResult testPartyEmail = GetTestPartyResult_Email(this._validAppSettingsConfiguration, Guid.Empty, Guid.NewGuid()); // Email should have priority over phone
+ PartyResults testPartyResults = GetTestPartyResults(testPartyEmail, testPartyPhone); // If both phone and email are preferred, email would have priority
// Act
(PartyResult actualParty, DistributionChannels actualDistChannel, string actualEmailAddress, string actualPhoneNumber)
@@ -145,7 +107,7 @@ public void Party_Method_ForExistingResults_Without_MatchingPreferredAddress_Ret
}
[Test]
- public void Party_Method_ForExistingResults_With_MatchingPreferredAddress_ReturnsExpectedResult_Phone()
+ public void Party_ForMany_ExistingResults_With_MatchingPreferredAddress_ReturnsExpectedResult_Phone()
{
// Arrange
var testId = Guid.NewGuid();
@@ -162,13 +124,13 @@ public void Party_Method_ForExistingResults_With_MatchingPreferredAddress_Return
{
Assert.That(actualParty, Is.EqualTo(testParty));
Assert.That(actualDistChannel, Is.EqualTo(DistributionChannels.Sms));
- Assert.That(actualEmailAddress, Is.Empty); // Since email is priority, phone should be ignored
- Assert.That(actualPhoneNumber, Is.EqualTo($"first_{TestPhone}"));
+ Assert.That(actualEmailAddress, Is.Empty); // Only phone is provided
+ Assert.That(actualPhoneNumber, Is.EqualTo($"second_{TestPhone}")); // The preferred address was found
});
}
[Test]
- public void Party_Method_ForExistingResults_Without_MatchingPreferredAddress_ReturnsExpectedResult_Phone_FirstEncountered()
+ public void Party_ForMany_ExistingResults_Without_MatchingPreferredAddress_ReturnsExpectedResult_Phone_FirstEncountered()
{
// Arrange
PartyResult testParty = GetTestPartyResult_Phone(this._validAppSettingsConfiguration, Guid.Empty, Guid.NewGuid());
@@ -183,14 +145,166 @@ public void Party_Method_ForExistingResults_Without_MatchingPreferredAddress_Ret
{
Assert.That(actualParty, Is.EqualTo(testParty));
Assert.That(actualDistChannel, Is.EqualTo(DistributionChannels.Sms));
- Assert.That(actualEmailAddress, Is.Empty); // Since email is priority, phone should be ignored
- Assert.That(actualPhoneNumber, Is.EqualTo($"first_{TestPhone}"));
+ Assert.That(actualEmailAddress, Is.Empty); // Only phone is provided
+ Assert.That(actualPhoneNumber, Is.EqualTo($"first_{TestPhone}")); // First encountered phone is returned because the preferred address couldn't be determined
});
}
#endregion
+ #region Party (processing single role)
+ [Test]
+ public void Party_ForSingle_ExistingResult_ButMissingAddress_ThrowsHttpRequestException()
+ {
+ // Arrange
+ PartyResult partyResult = new(); // Empty digital addresses
+
+ // Act & Assert
+ AssertThrows(this._validAppSettingsConfiguration, partyResult, Resources.HttpRequest_ERROR_NoDigitalAddresses);
+ }
+
+ [Test]
+ public void Party_ForSingle_ExistingResult_ButEmptyDigitalAddresses_ThrowsHttpRequestException()
+ {
+ // Arrange
+ PartyResult partyResult = GetTestPartyResult_None(this._validAppSettingsConfiguration); // Missing e-mails and phone numbers
+
+ // Act & Assert
+ AssertThrows(this._validAppSettingsConfiguration, partyResult, Resources.HttpRequest_ERROR_NoDigitalAddresses);
+ }
+
+ [Test]
+ public void Party_ForSingle_ExistingResult_With_MatchingPreferredAddress_ReturnsExpectedResult_Email()
+ {
+ // Arrange
+ var testId = Guid.NewGuid();
+
+ PartyResult testPartyEmail = GetTestPartyResult_Email(this._validAppSettingsConfiguration, testId, testId); // Email should have priority over phone
+
+ // Act
+ (PartyResult actualParty, DistributionChannels actualDistChannel, string actualEmailAddress, string actualPhoneNumber)
+ = PartyResults.Party(this._validAppSettingsConfiguration, testPartyEmail);
+
+ // Assert
+ Assert.Multiple(() =>
+ {
+ Assert.That(actualParty, Is.EqualTo(testPartyEmail));
+ Assert.That(actualDistChannel, Is.EqualTo(DistributionChannels.Email));
+ Assert.That(actualEmailAddress, Is.EqualTo($"second_{TestEmail}")); // The preferred address was found
+ Assert.That(actualPhoneNumber, Is.Empty); // Only email is provided
+ });
+ }
+
+ [Test]
+ public void Party_ForSingle_ExistingResult_Without_MatchingPreferredAddress_ReturnsExpectedResult_Email_FirstEncountered_PriorityOverPhone()
+ {
+ // Arrange
+ PartyResult testPartyEmail = GetTestPartyResult_Email(this._validAppSettingsConfiguration, Guid.Empty, Guid.NewGuid()); // Email should have priority over phone
+
+ // Act
+ (PartyResult actualParty, DistributionChannels actualDistChannel, string actualEmailAddress, string actualPhoneNumber)
+ = PartyResults.Party(this._validAppSettingsConfiguration, testPartyEmail);
+
+ // Assert
+ Assert.Multiple(() =>
+ {
+ Assert.That(actualParty, Is.EqualTo(testPartyEmail));
+ Assert.That(actualDistChannel, Is.EqualTo(DistributionChannels.Email));
+ Assert.That(actualEmailAddress, Is.EqualTo($"first_{TestEmail}")); // First encountered email is returned because the preferred address couldn't be determined
+ Assert.That(actualPhoneNumber, Is.Empty); // Only email is provided
+ });
+ }
+
+ [Test]
+ public void Party_ForSingle_ExistingResult_With_MatchingPreferredAddress_ReturnsExpectedResult_Phone()
+ {
+ // Arrange
+ var testId = Guid.NewGuid();
+
+ PartyResult testParty = GetTestPartyResult_Phone(this._validAppSettingsConfiguration, testId, testId);
+
+ // Act
+ (PartyResult actualParty, DistributionChannels actualDistChannel, string actualEmailAddress, string actualPhoneNumber)
+ = PartyResults.Party(this._validAppSettingsConfiguration, testParty);
+
+ // Assert
+ Assert.Multiple(() =>
+ {
+ Assert.That(actualParty, Is.EqualTo(testParty));
+ Assert.That(actualDistChannel, Is.EqualTo(DistributionChannels.Sms));
+ Assert.That(actualEmailAddress, Is.Empty); // Only phone is provided
+ Assert.That(actualPhoneNumber, Is.EqualTo($"second_{TestPhone}")); // The preferred address was found
+ });
+ }
+
+ [Test]
+ public void Party_ForSingle_ExistingResult_Without_MatchingPreferredAddress_ReturnsExpectedResult_Phone_FirstEncountered()
+ {
+ // Arrange
+ PartyResult testParty = GetTestPartyResult_Phone(this._validAppSettingsConfiguration, Guid.Empty, Guid.NewGuid());
+
+ // Act
+ (PartyResult actualParty, DistributionChannels actualDistChannel, string actualEmailAddress, string actualPhoneNumber)
+ = PartyResults.Party(this._validAppSettingsConfiguration, testParty);
+
+ // Assert
+ Assert.Multiple(() =>
+ {
+ Assert.That(actualParty, Is.EqualTo(testParty));
+ Assert.That(actualDistChannel, Is.EqualTo(DistributionChannels.Sms));
+ Assert.That(actualEmailAddress, Is.Empty); // Only phone is provided
+ Assert.That(actualPhoneNumber, Is.EqualTo($"first_{TestPhone}")); // First encountered phone is returned because the preferred address couldn't be determined
+ });
+ }
+ #endregion
+
#region Helper methods
+ ///
+ /// In this set of test data there are no phone numbers or e-mails.
+ ///
+ private static PartyResult GetTestPartyResult_None(WebApiConfiguration testConfiguration)
+ {
+
+ var partyId = Guid.NewGuid();
+ Guid firstAddressId = GetUniqueId(partyId);
+ Guid secondAddressId = GetUniqueId(firstAddressId);
+
+ return new PartyResult
+ {
+ PreferredDigitalAddress = new DigitalAddressShort
+ {
+ Id = partyId
+ },
+ Identification = new PartyIdentification
+ {
+ Details = new PartyDetails
+ {
+ Name = "Samantha",
+ SurnamePrefix = string.Empty,
+ Surname = "Rogers"
+ }
+ },
+ Expansion = new Expansion
+ {
+ DigitalAddresses = new List
+ {
+ new()
+ {
+ Id = firstAddressId,
+ Value = string.Empty, // Just empty address
+ Type = testConfiguration.AppSettings.Variables.PhoneGenericDescription()
+ },
+ new()
+ {
+ Id = secondAddressId,
+ Value = string.Empty, // Just empty address
+ Type = testConfiguration.AppSettings.Variables.EmailGenericDescription()
+ }
+ }
+ }
+ };
+ }
+
private const string TestEmail = "john.doe@gmail.com";
private const string TestPhone = "+44 7911 123456";
@@ -241,23 +355,23 @@ private static PartyResult GetTestPartyResult_Email(WebApiConfiguration configur
Type = "Facebook"
},
- new() // Never preferred address but matching 1st phone (e-mails have priority)
+ new() // Not preferred address but valid phone (e-mails have priority => keep processing)
{
Id = GetUniqueId(addressId),
Value = TestPhone,
Type = configuration.AppSettings.Variables.PhoneGenericDescription()
},
- new() // Can be preferred address or not (addressId) matching 1st e-mail
+ new() // Not preferred address but valid e-mail (should be used if the preferred e-mail is not found => until then keep processing)
{
- Id = addressId,
+ Id = GetUniqueId(addressId),
Value = $"first_{TestEmail}",
Type = configuration.AppSettings.Variables.EmailGenericDescription()
},
- new() // Never preferred address but matching 2nd e-mail (not first encountered)
+ new() // Can be preferred address with valid e-mail (not first encountered, should be returned if preferred)
{
- Id = GetUniqueId(addressId),
+ Id = addressId,
Value = $"second_{TestEmail}",
Type = configuration.AppSettings.Variables.EmailGenericDescription()
}
@@ -313,16 +427,16 @@ private static PartyResult GetTestPartyResult_Phone(WebApiConfiguration configur
Type = "Facebook"
},
- new() // Can be preferred address or not (addressId) matching 1st phone
+ new() // Not preferred address but valid phone (should be used if the preferred phone is not found => until then keep processing)
{
- Id = addressId,
+ Id = GetUniqueId(addressId),
Value = $"first_{TestPhone}",
Type = configuration.AppSettings.Variables.PhoneGenericDescription()
},
- new() // Never preferred address but matching 2nd phone (not first encountered)
+ new() // Can be preferred address with valid phone (not first encountered, should be returned if preferred)
{
- Id = GetUniqueId(addressId),
+ Id = addressId,
Value = $"second_{TestPhone}",
Type = configuration.AppSettings.Variables.PhoneGenericDescription()
}
@@ -366,6 +480,18 @@ private static void AssertThrows(WebApiConfiguration configuration,
Assert.That(exception?.Message, Is.EqualTo(exceptionMessage));
});
}
+
+ private static void AssertThrows(WebApiConfiguration configuration, PartyResult partyResult, string exceptionMessage)
+ where TException : Exception
+ {
+ Assert.Multiple(() =>
+ {
+ TException? exception = Assert.Throws(() =>
+ PartyResults.Party(configuration, partyResult));
+
+ Assert.That(exception?.Message, Is.EqualTo(exceptionMessage));
+ });
+ }
#endregion
}
}
\ No newline at end of file
diff --git a/EventsHandler/Tests/UnitTests/EventsHandler.Tests/Mapping/Models/POCOs/OpenZaak/v2/CaseRolesTests.cs b/EventsHandler/Tests/UnitTests/EventsHandler.Tests/Mapping/Models/POCOs/OpenZaak/v2/CaseRolesTests.cs
index 3b1e99dd..9a65c94c 100644
--- a/EventsHandler/Tests/UnitTests/EventsHandler.Tests/Mapping/Models/POCOs/OpenZaak/v2/CaseRolesTests.cs
+++ b/EventsHandler/Tests/UnitTests/EventsHandler.Tests/Mapping/Models/POCOs/OpenZaak/v2/CaseRolesTests.cs
@@ -58,7 +58,7 @@ public void Citizen_Method_ForExistingResults_WithSingleInitiatorRole_ReturnsCit
new CaseRole { InitiatorRole = existingInitiatorRole, Party = expectedCitizen }); // Unique matching result
// Act
- PartyData actualParty = caseRoles.CaseRole(this._testConfiguration).Party;
+ PartyData actualParty = caseRoles.CaseRole(this._testConfiguration).Party!.Value;
// Assert
Assert.That(actualParty, Is.EqualTo(expectedCitizen));
diff --git a/EventsHandler/Tests/UnitTests/EventsHandler.Tests/Services/DataProcessing/Strategy/Implementations/TaskAssignedScenarioTests.cs b/EventsHandler/Tests/UnitTests/EventsHandler.Tests/Services/DataProcessing/Strategy/Implementations/TaskAssignedScenarioTests.cs
index e0b5f4e7..66297d00 100644
--- a/EventsHandler/Tests/UnitTests/EventsHandler.Tests/Services/DataProcessing/Strategy/Implementations/TaskAssignedScenarioTests.cs
+++ b/EventsHandler/Tests/UnitTests/EventsHandler.Tests/Services/DataProcessing/Strategy/Implementations/TaskAssignedScenarioTests.cs
@@ -71,7 +71,7 @@ public void TestsCleanup()
Status = TaskStatuses.Closed
};
- private static readonly CommonTaskData s_taskOpenNotAssignedToPerson = new()
+ private static readonly CommonTaskData s_taskOpenNotAssignedToAnyone = new()
{
Status = TaskStatuses.Open,
Identification = new Identification
@@ -81,6 +81,8 @@ public void TestsCleanup()
};
private const string TestTaskTitle = "Test title";
+
+ // Tasks assigned to someone with expiration date
private static readonly CommonTaskData s_taskOpenAssignedToPersonWithoutExpirationDate = new()
{
Title = TestTaskTitle,
@@ -91,7 +93,18 @@ public void TestsCleanup()
Type = IdTypes.Bsn
}
};
+ private static readonly CommonTaskData s_taskOpenAssignedToOrganizationWithoutExpirationDate = new()
+ {
+ Title = TestTaskTitle,
+ Status = TaskStatuses.Open,
+ // Missing "ExpirationDate" => default
+ Identification = new Identification
+ {
+ Type = IdTypes.Kvk
+ }
+ };
+ // Tasks assigned to someone without expiration date
private static readonly CommonTaskData s_taskOpenAssignedToPersonWithExpirationDate = new()
{
Title = TestTaskTitle,
@@ -102,6 +115,16 @@ public void TestsCleanup()
Type = IdTypes.Bsn
}
};
+ private static readonly CommonTaskData s_taskOpenAssignedToOrganizationWithExpirationDate = new()
+ {
+ Title = TestTaskTitle,
+ Status = TaskStatuses.Open,
+ ExpirationDate = new DateTime(2024, 7, 24, 14, 10, 40, DateTimeKind.Utc),
+ Identification = new Identification
+ {
+ Type = IdTypes.Kvk
+ }
+ };
private const string TestEmailAddress = "test@email.com";
private const string TestPhoneNumber = "911";
@@ -131,35 +154,36 @@ public void TryGetDataAsync_ValidTaskType_Closed_ThrowsAbortedNotifyingException
});
}
- //[Test]
- //public void TryGetDataAsync_ValidTaskType_Open_NotAssignedToPerson_ThrowsAbortedNotifyingException()
- //{
- // // Arrange
- // INotifyScenario scenario = ArrangeTaskScenario_TryGetData(
- // DistributionChannels.Email,
- // s_taskOpenNotAssignedToPerson,
- // true,
- // true);
-
- // // Act & Assert
- // Assert.Multiple(() =>
- // {
- // AbortedNotifyingException? exception =
- // Assert.ThrowsAsync(() => scenario.TryGetDataAsync(s_validNotification));
- // Assert.That(exception?.Message.StartsWith(Resources.Processing_ABORT_DoNotSendNotification_TaskNotPerson), Is.True);
- // Assert.That(exception?.Message.EndsWith(Resources.Processing_ABORT), Is.True);
-
- // VerifyGetDataMethodCalls(1, 1, 0, 0, 0);
- // });
- //}
-
[Test]
- public void TryGetDataAsync_ValidTaskType_Open_AssignedToPerson_NotWhitelisted_ThrowsAbortedNotifyingException()
+ public void TryGetDataAsync_ValidTaskType_Open_NotAssignedToAnyone_ThrowsAbortedNotifyingException()
+ {
+ // Arrange
+ INotifyScenario scenario = ArrangeTaskScenario_TryGetData(
+ DistributionChannels.Email,
+ s_taskOpenNotAssignedToAnyone,
+ true,
+ true);
+
+ // Act & Assert
+ Assert.Multiple(() =>
+ {
+ AbortedNotifyingException? exception =
+ Assert.ThrowsAsync(() => scenario.TryGetDataAsync(s_validNotification));
+ Assert.That(exception?.Message.StartsWith(Resources.Processing_ABORT_DoNotSendNotification_TaskIdTypeNotSupported), Is.True);
+ Assert.That(exception?.Message.EndsWith(Resources.Processing_ABORT), Is.True);
+
+ VerifyGetDataMethodCalls(1, 1, 0, 0, 0);
+ });
+ }
+
+ [TestCase(IdTypes.Bsn)] // Person
+ [TestCase(IdTypes.Kvk)] // Organization
+ public void TryGetDataAsync_ValidTaskType_Open_AssignedToEntity_NotWhitelisted_ThrowsAbortedNotifyingException(IdTypes idType)
{
// Arrange
INotifyScenario scenario = ArrangeTaskScenario_TryGetData(
DistributionChannels.Email,
- s_taskOpenAssignedToPersonWithExpirationDate,
+ testTask: GetOpenAssignedTaskWithExpirationDate(idType),
isCaseTypeIdWhitelisted: false,
isNotificationExpected: true);
@@ -179,14 +203,15 @@ public void TryGetDataAsync_ValidTaskType_Open_AssignedToPerson_NotWhitelisted_T
VerifyGetDataMethodCalls(1, 1, 1, 0, 0);
});
}
-
- [Test]
- public void TryGetDataAsync_ValidTaskType_Open_AssignedToPerson_Whitelisted_InformSetToFalse_ThrowsAbortedNotifyingException()
+
+ [TestCase(IdTypes.Bsn)] // Person
+ [TestCase(IdTypes.Kvk)] // Organization
+ public void TryGetDataAsync_ValidTaskType_Open_AssignedToEntity_Whitelisted_InformSetToFalse_ThrowsAbortedNotifyingException(IdTypes idType)
{
// Arrange
INotifyScenario scenario = ArrangeTaskScenario_TryGetData(
DistributionChannels.Email,
- s_taskOpenAssignedToPersonWithExpirationDate,
+ testTask: GetOpenAssignedTaskWithExpirationDate(idType),
isCaseTypeIdWhitelisted: true,
isNotificationExpected: false);
@@ -202,16 +227,21 @@ public void TryGetDataAsync_ValidTaskType_Open_AssignedToPerson_Whitelisted_Info
});
}
- [TestCase(DistributionChannels.None)]
- [TestCase(DistributionChannels.Unknown)]
- [TestCase((DistributionChannels)(-1))]
- public async Task TryGetDataAsync_ValidTaskType_Open_AssignedToPerson_Whitelisted_InformSetToTrue_WithInvalidDistChannel_ReturnsFailure(
- DistributionChannels invalidDistributionChannel)
+ // Person
+ [TestCase(DistributionChannels.None, IdTypes.Bsn)]
+ [TestCase(DistributionChannels.Unknown, IdTypes.Bsn)]
+ [TestCase((DistributionChannels)(-1), IdTypes.Bsn)]
+ // Organization
+ [TestCase(DistributionChannels.None, IdTypes.Kvk)]
+ [TestCase(DistributionChannels.Unknown, IdTypes.Kvk)]
+ [TestCase((DistributionChannels)(-1), IdTypes.Kvk)]
+ public async Task TryGetDataAsync_ValidTaskType_Open_AssignedToEntity_Whitelisted_InformSetToTrue_WithInvalidDistChannel_ReturnsFailure(
+ DistributionChannels invalidDistributionChannel, IdTypes idType)
{
// Arrange
INotifyScenario scenario = ArrangeTaskScenario_TryGetData(
invalidDistributionChannel,
- s_taskOpenAssignedToPersonWithExpirationDate,
+ testTask: GetOpenAssignedTaskWithExpirationDate(idType),
true,
true);
@@ -229,16 +259,21 @@ public async Task TryGetDataAsync_ValidTaskType_Open_AssignedToPerson_Whiteliste
});
}
- [TestCase(DistributionChannels.Email, NotifyMethods.Email, 1, TestEmailAddress)]
- [TestCase(DistributionChannels.Sms, NotifyMethods.Sms, 1, TestPhoneNumber)]
- [TestCase(DistributionChannels.Both, null, 2, TestEmailAddress + TestPhoneNumber)]
- public async Task TryGetDataAsync_ValidTaskType_Open_AssignedToPerson_Whitelisted_InformSetToTrue_WithValidDistChannels_ReturnsSuccess(
- DistributionChannels testDistributionChannel, NotifyMethods? expectedNotificationMethod, int notifyDataCount, string expectedContactDetails)
+ // Person
+ [TestCase(DistributionChannels.Email, IdTypes.Bsn, NotifyMethods.Email, 1, TestEmailAddress)]
+ [TestCase(DistributionChannels.Sms, IdTypes.Bsn, NotifyMethods.Sms, 1, TestPhoneNumber)]
+ [TestCase(DistributionChannels.Both, IdTypes.Bsn, null, 2, TestEmailAddress + TestPhoneNumber)]
+ // Organization
+ [TestCase(DistributionChannels.Email, IdTypes.Kvk, NotifyMethods.Email, 1, TestEmailAddress)]
+ [TestCase(DistributionChannels.Sms, IdTypes.Kvk, NotifyMethods.Sms, 1, TestPhoneNumber)]
+ [TestCase(DistributionChannels.Both, IdTypes.Kvk, null, 2, TestEmailAddress + TestPhoneNumber)]
+ public async Task TryGetDataAsync_ValidTaskType_Open_AssignedToEntity_Whitelisted_InformSetToTrue_WithValidDistChannels_ReturnsSuccess(
+ DistributionChannels testDistributionChannel, IdTypes idType, NotifyMethods? expectedNotificationMethod, int notifyDataCount, string expectedContactDetails)
{
// Arrange
INotifyScenario scenario = ArrangeTaskScenario_TryGetData(
testDistributionChannel,
- s_taskOpenAssignedToPersonWithExpirationDate,
+ testTask: GetOpenAssignedTaskWithExpirationDate(idType),
isCaseTypeIdWhitelisted: true,
isNotificationExpected: true);
@@ -286,16 +321,22 @@ public async Task TryGetDataAsync_ValidTaskType_Open_AssignedToPerson_Whiteliste
#endregion
#region GetPersonalizationAsync()
- [TestCase(DistributionChannels.Email, false, "-", "no")]
- [TestCase(DistributionChannels.Email, true, "2024-07-24", "yes")]
- [TestCase(DistributionChannels.Sms, false, "-", "no")]
- [TestCase(DistributionChannels.Sms, true, "2024-07-24", "yes")]
+ // Person
+ [TestCase(DistributionChannels.Email, IdTypes.Bsn, false, "-", "no")]
+ [TestCase(DistributionChannels.Email, IdTypes.Bsn, true, "2024-07-24", "yes")]
+ [TestCase(DistributionChannels.Sms, IdTypes.Bsn, false, "-", "no")]
+ [TestCase(DistributionChannels.Sms, IdTypes.Bsn, true, "2024-07-24", "yes")]
+ // Organization
+ [TestCase(DistributionChannels.Email, IdTypes.Kvk, false, "-", "no")]
+ [TestCase(DistributionChannels.Email, IdTypes.Kvk, true, "2024-07-24", "yes")]
+ [TestCase(DistributionChannels.Sms, IdTypes.Kvk, false, "-", "no")]
+ [TestCase(DistributionChannels.Sms, IdTypes.Kvk, true, "2024-07-24", "yes")]
public async Task GetPersonalizationAsync_SpecificDateTime_ReturnsExpectedPersonalization(
- DistributionChannels testDistributionChannel, bool isExpirationDateGiven, string testExpirationDate, string isExpirationDateGivenText)
+ DistributionChannels testDistributionChannel, IdTypes idType, bool isExpirationDateGiven, string testExpirationDate, string isExpirationDateGivenText)
{
CommonTaskData testTaskData = isExpirationDateGiven
- ? s_taskOpenAssignedToPersonWithExpirationDate
- : s_taskOpenAssignedToPersonWithoutExpirationDate;
+ ? GetOpenAssignedTaskWithExpirationDate(idType)
+ : GetOpenAssignedTaskWithoutExpirationDate(idType);
INotifyScenario scenario = ArrangeTaskScenario_TryGetData(
testDistributionChannel,
@@ -590,5 +631,21 @@ private void VerifyProcessDataMethodCalls(int sendEmailInvokeCount, int sendSmsI
VerifyGetDataMethodCalls(0, 0, 0, 0, 0);
}
#endregion
+
+ #region Helper methods
+ private static CommonTaskData GetOpenAssignedTaskWithExpirationDate(IdTypes idType)
+ {
+ return idType == IdTypes.Bsn ? s_taskOpenAssignedToPersonWithExpirationDate :
+ idType == IdTypes.Kvk ? s_taskOpenAssignedToOrganizationWithExpirationDate
+ : default;
+ }
+
+ private static CommonTaskData GetOpenAssignedTaskWithoutExpirationDate(IdTypes idType)
+ {
+ return idType == IdTypes.Bsn ? s_taskOpenAssignedToPersonWithoutExpirationDate :
+ idType == IdTypes.Kvk ? s_taskOpenAssignedToOrganizationWithoutExpirationDate
+ : default;
+ }
+ #endregion
}
}
\ No newline at end of file
diff --git a/EventsHandler/Tests/UnitTests/EventsHandler.Tests/Services/Serialization/SpecificSerializerTests.cs b/EventsHandler/Tests/UnitTests/EventsHandler.Tests/Services/Serialization/SpecificSerializerTests.cs
index 05778710..6d208695 100644
--- a/EventsHandler/Tests/UnitTests/EventsHandler.Tests/Services/Serialization/SpecificSerializerTests.cs
+++ b/EventsHandler/Tests/UnitTests/EventsHandler.Tests/Services/Serialization/SpecificSerializerTests.cs
@@ -126,33 +126,26 @@ public void Deserialize_CaseType_ValidJson_ReturnsExpectedModel(string jsonAttri
}
[Test]
- public void Deserialize_IJsonSerializable_From_EmptyJson_ThrowsJsonException_ListsRequiredProperties() // NOTE: Simple model
+ public void Deserialize_CaseRole_ValidJson_Null_ReturnsExpectedModel() // NOTE: Handles nullable property
{
- // Act & Assert
- foreach ((int id, Action deserialization, string? targetName, string? expectedResult) in GetSerializationTests(DefaultValues.Models.EmptyJson))
- {
- Assert.Multiple(() =>
- {
- JsonException? exception = Assert.Throws(() => deserialization.Invoke());
-
- string expectedMessage =
- $"The given value cannot be deserialized into dedicated target object | " +
- $"Target: {targetName} | " +
- $"Value: {{}} | " +
- $"Required properties: {expectedResult}";
-
- Assert.That(exception?.Message, Is.EqualTo(expectedMessage), message: $"Test #{id}");
- });
- }
+ // Arrange
+ const string testJson =
+ $"{{" +
+ $"\"betrokkene\":null," + // Should be deserialized as default not null
+ $"\"omschrijvingGeneriek\":\"{TestString}\"," +
+ $"\"betrokkeneIdentificatie\":null" + // Can be null
+ $"}}";
- return;
+ // Act
+ CaseRole actualResult = this._serializer.Deserialize(testJson);
- IEnumerable<(int Id, Action Deserialization, string TargetName, string ExpectedResult)> GetSerializationTests(string testJson)
+ // Assert
+ Assert.Multiple(() =>
{
- yield return (1, () => this._serializer.Deserialize(testJson), nameof(CaseType), "omschrijving, omschrijvingGeneriek, zaaktypeIdentificatie, isEindstatus, informeren");
- yield return (2, () => this._serializer.Deserialize(testJson), nameof(PartyResult), "url, voorkeursDigitaalAdres.uuid, partijIdentificatie.contactnaam.voornaam, _expand.digitaleAdressen.uuid, _expand.digitaleAdressen.adres, _expand.digitaleAdressen.soortDigitaalAdres");
- yield return (3, () => this._serializer.Deserialize(testJson), nameof(CommonTaskData), "CaseUri, CaseId, Title, Status, ExpirationDate, Identification.type, Identification.value");
- }
+ Assert.That(actualResult.InvolvedPartyUri, Is.EqualTo(DefaultValues.Models.EmptyUri));
+ Assert.That(actualResult.InitiatorRole, Is.EqualTo(TestString));
+ Assert.That(actualResult.Party, Is.Null);
+ });
}
private const string TaskDataJsonTheHague =
@@ -374,6 +367,108 @@ public void Deserialize_ContactMoment_PartiallyValidJson_ReturnsExpectedModel()
}
#endregion
+ #region Errors
+ internal struct Example : IJsonSerializable
+ {
+ [JsonRequired]
+ [JsonInclude]
+ public string Name { get; internal set; }
+
+ [JsonRequired]
+ [JsonInclude]
+ public int Age { get; internal set; }
+
+ [JsonRequired]
+ [JsonInclude]
+ public bool IsAdmin { get; internal set; }
+
+ [JsonRequired]
+ [JsonInclude]
+ public NestedExample Nested { get; private set; }
+
+ [JsonRequired]
+ [JsonInclude]
+ public NestedExample[] Array { get; private set; }
+
+ [JsonRequired]
+ [JsonInclude]
+ public List List { get; private set; }
+ }
+
+ internal struct NestedExample : IJsonSerializable
+ {
+ [JsonRequired]
+ [JsonInclude]
+ public string Name2 { get; internal set; }
+
+ [JsonRequired]
+ [JsonInclude]
+ public int Age2 { get; internal set; }
+
+ [JsonRequired]
+ [JsonInclude]
+ public int IsAdmin2 { get; internal set; }
+ }
+
+ [Test]
+ public void Deserialize_IJsonSerializable_From_IncompleteJson_ThrowsJsonException_ListsRequiredProperties()
+ {
+ // Arrange
+ const string serialized = "{\"Name\":\"Aliana\",\"Age\":null,\"IsAdmin\":null}";
+
+ // Act & Assert
+ Assert.Multiple(() =>
+ {
+ JsonException? exception = Assert.Throws(() => this._serializer.Deserialize(serialized));
+
+ const string expectedMessage = "The given JSON cannot be deserialized | " +
+ "Target model: 'Example.cs' | " +
+ "Failed: '$.Age' | " +
+ "Reason: Cannot get the value of a token type 'Null' as a number. | " +
+ "All required properties: " +
+ "'Name', 'Age', 'IsAdmin', " +
+ "'Nested.Name2', 'Nested.Age2', 'Nested.IsAdmin2', " +
+ "'Array.Name2', 'Array.Age2', 'Array.IsAdmin2', " +
+ "'List.Name2', 'List.Age2', 'List.IsAdmin2' | " +
+ "Source JSON: {\"Name\":\"Aliana\",\"Age\":null,\"IsAdmin\":null}";
+
+ Assert.That(exception?.Message, Is.EqualTo(expectedMessage));
+ });
+ }
+
+ [Test]
+ public void Deserialize_IJsonSerializable_From_EmptyJson_ThrowsJsonException_ListsRequiredProperties() // NOTE: Simple model
+ {
+ // Act & Assert
+ foreach ((int id, Action deserialization, string? targetName, string failed, string? expectedResult) in GetSerializationTests(DefaultValues.Models.EmptyJson))
+ {
+ Assert.Multiple(() =>
+ {
+ JsonException? exception = Assert.Throws(() => deserialization.Invoke());
+
+ string expectedMessage =
+ $"The given JSON cannot be deserialized | " +
+ $"Target model: '{targetName}.cs' | " +
+ $"Failed: '{failed}' | " +
+ $"Reason: {Properties.Resources.Deserialization_ERROR_CannotDeserialize_RequiredProperties} | " +
+ $"All required properties: {expectedResult} | " +
+ $"Source JSON: {{}}";
+
+ Assert.That(exception?.Message, Is.EqualTo(expectedMessage), message: $"Test #{id}");
+ });
+ }
+
+ return;
+
+ IEnumerable<(int Id, Action Deserialization, string TargetName, string Failed, string ExpectedResult)> GetSerializationTests(string testJson)
+ {
+ yield return (1, () => this._serializer.Deserialize(testJson), nameof(CaseType), "omschrijving, omschrijvingGeneriek, zaaktypeIdentificatie", "'omschrijving', 'omschrijvingGeneriek', 'zaaktypeIdentificatie', 'isEindstatus', 'informeren'");
+ yield return (2, () => this._serializer.Deserialize(testJson), nameof(PartyResult), "url, voorkeursDigitaalAdres, partijIdentificatie, _expand", "'url', 'voorkeursDigitaalAdres.uuid', 'partijIdentificatie.contactnaam.voornaam', '_expand.digitaleAdressen.uuid', '_expand.digitaleAdressen.adres', '_expand.digitaleAdressen.soortDigitaalAdres'");
+ yield return (3, () => this._serializer.Deserialize(testJson), nameof(CommonTaskData), "record", "'CaseUri', 'CaseId', 'Title', 'Status', 'ExpirationDate', 'Identification.type', 'Identification.value'");
+ }
+ }
+ #endregion
+
#region Serialize
[TestCase(true)]
[TestCase(false)]
@@ -473,6 +568,31 @@ public void Serialize_CaseType_ValidModel_ReturnsExpectedJson()
Assert.That(actualResult, Is.EqualTo(expectedResult));
}
+ [Test]
+ public void Serialize_CaseRole_ValidModel_ReturnsExpectedJson() // Nullable property is accepted
+ {
+ // Arrange
+ var testModel = new CaseRole
+ {
+ InvolvedPartyUri = new Uri(TestUrl),
+ InitiatorRole = TestString,
+ Party = null
+ };
+
+ // Act
+ string actualResult = this._serializer.Serialize(testModel);
+
+ // Assert
+ const string expectedResult =
+ $"{{" +
+ $"\"betrokkene\":\"{TestUrl}\"," +
+ $"\"omschrijvingGeneriek\":\"{TestString}\"," +
+ $"\"betrokkeneIdentificatie\":null" +
+ $"}}";
+
+ Assert.That(actualResult, Is.EqualTo(expectedResult));
+ }
+
[TestCase(true)]
[TestCase(false)]
public void Serialize_TaskObject_Hague_Default_ReturnsExpectedJson(bool isDefault) // NOTE: Very complex model: nested objects (with objects and enums) should be initialized as well
diff --git a/NotifyNL.sln.DotSettings b/NotifyNL.sln.DotSettings
index e325ca4b..e301661c 100644
--- a/NotifyNL.sln.DotSettings
+++ b/NotifyNL.sln.DotSettings
@@ -15,6 +15,7 @@
True
True
True
+ True
True
True
True
@@ -26,6 +27,7 @@
True
True
True
+ True
True
True
True
@@ -188,6 +190,7 @@
True
True
True
+ True
True
True
True