Skip to content

Commit

Permalink
feat(clearingHouse): Integrate the Tagus Release V2 Api (#1220)
Browse files Browse the repository at this point in the history
* chore(clearingHouse): Integrate the Tagus Release V2 Api
* change CH url to V2 from V1
* adjust Response objects
* adjust unit tests
Ref: 1217
---------
Co-authored-by: Norbert Truchsess <[email protected]>
  • Loading branch information
tfjanjua authored Feb 6, 2025
1 parent f2f713c commit 54fbbe2
Show file tree
Hide file tree
Showing 19 changed files with 348 additions and 237 deletions.
34 changes: 26 additions & 8 deletions docs/api/administration-service.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6124,18 +6124,18 @@ components:
ClearinghouseResponseData:
type: object
properties:
bpn:
type: string
status:
$ref: '#/components/schemas/ClearinghouseResponseStatus'
message:
validationMode:
type: string
nullable: true
validationUnits:
type: array
items:
$ref: '#/components/schemas/ValidationUnits'
additionalProperties: false
ClearinghouseResponseStatus:
enum:
- CONFIRM
- DECLINE
- VALID
- INVALID
- INCONCLUSIVE
type: string
CompanyAddressDetailData:
type: object
Expand Down Expand Up @@ -7737,6 +7737,14 @@ components:
type: string
nullable: true
additionalProperties: false
Reason:
type: object
properties:
message:
type: string
detailMessage:
type: string
additionalProperties: false
RegistrationDeclineData:
type: object
properties:
Expand Down Expand Up @@ -8107,6 +8115,16 @@ components:
items:
$ref: '#/components/schemas/ErrorDetails'
additionalProperties: false
ValidationUnits:
type: object
properties:
result:
$ref: '#/components/schemas/ClearinghouseResponseStatus'
type:
type: string
reason:
$ref: '#/components/schemas/Reason'
additionalProperties: false
securitySchemes:
Bearer:
type: apiKey
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,9 @@ public interface IRegistrationBusinessLogic
/// Processes the clearinghouse response
/// </summary>
/// <param name="data">the response data</param>
/// <param name="bpn">BusinessPartnerNumber of the responded data</param>
/// <param name="cancellationToken">cancellation token</param>
Task ProcessClearinghouseResponseAsync(ClearinghouseResponseData data, CancellationToken cancellationToken);
Task ProcessClearinghouseResponseAsync(ClearinghouseResponseData data, string bpn, CancellationToken cancellationToken);

/// <summary>
/// Processes the dim response
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -299,18 +299,18 @@ private async Task UpdateCompanyBpnInternal(Guid applicationId, string bpn)
private ProcessStepTypeId CreateWalletStep() => _settings.UseDimWallet ? ProcessStepTypeId.CREATE_DIM_WALLET : ProcessStepTypeId.CREATE_IDENTITY_WALLET;

/// <inheritdoc />
public async Task ProcessClearinghouseResponseAsync(ClearinghouseResponseData data, CancellationToken cancellationToken)
public async Task ProcessClearinghouseResponseAsync(ClearinghouseResponseData data, string bpn, CancellationToken cancellationToken)
{
logger.LogInformation("Process SelfDescription called with the following data {Data}", data.ToString().Replace(Environment.NewLine, string.Empty));
var result = await portalRepositories.GetInstance<IApplicationRepository>().GetSubmittedApplicationIdsByBpn(data.BusinessPartnerNumber.ToUpper()).ToListAsync(cancellationToken).ConfigureAwait(false);
logger.LogInformation("Process SelfDescription called with the following data {Data} and bpn {Bpn}", data.ToString().Replace(Environment.NewLine, string.Empty), bpn.ToString().Replace(Environment.NewLine, string.Empty));
var result = await portalRepositories.GetInstance<IApplicationRepository>().GetSubmittedApplicationIdsByBpn(bpn.ToUpper()).ToListAsync(cancellationToken).ConfigureAwait(false);
if (!result.Any())
{
throw NotFoundException.Create(AdministrationRegistrationErrors.REGISTRATION_NOT_COMP_APP_BPN_STATUS_SUBMIT, new ErrorParameter[] { new("businessPartnerNumber", data.BusinessPartnerNumber) });
throw NotFoundException.Create(AdministrationRegistrationErrors.REGISTRATION_NOT_COMP_APP_BPN_STATUS_SUBMIT, new ErrorParameter[] { new("businessPartnerNumber", bpn) });
}

if (result.Count > 1)
{
throw ConflictException.Create(AdministrationRegistrationErrors.REGISTRATION_CONFLICT_APP_STATUS_STATUS_SUBMIT_FOUND_BPN, new ErrorParameter[] { new("businessPartnerNumber", data.BusinessPartnerNumber) });
throw ConflictException.Create(AdministrationRegistrationErrors.REGISTRATION_CONFLICT_APP_STATUS_STATUS_SUBMIT_FOUND_BPN, new ErrorParameter[] { new("businessPartnerNumber", bpn) });
}

await clearinghouseBusinessLogic.ProcessEndClearinghouse(result.Single(), data, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
using Org.Eclipse.TractusX.Portal.Backend.Framework.Models;
using Org.Eclipse.TractusX.Portal.Backend.Framework.Web;
using Org.Eclipse.TractusX.Portal.Backend.IssuerComponent.Library.Models;
using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Authentication;
using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.DBAccess.Models;
using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.PortalEntities.Enums;
using Org.Eclipse.TractusX.Portal.Backend.SdFactory.Library.Models;
Expand Down Expand Up @@ -183,7 +184,7 @@ public async Task<NoContentResult> DeclineApplication([FromRoute] Guid applicati
[ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status404NotFound)]
public async Task<NoContentResult> ProcessClearinghouseResponse([FromBody] ClearinghouseResponseData responseData, CancellationToken cancellationToken)
{
await logic.ProcessClearinghouseResponseAsync(responseData, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None);
await this.WithBpn(bpn => logic.ProcessClearinghouseResponseAsync(responseData, bpn, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None));
return NoContent();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,23 @@

using Microsoft.Extensions.Options;
using Org.Eclipse.TractusX.Portal.Backend.Clearinghouse.Library.Models;
using Org.Eclipse.TractusX.Portal.Backend.Custodian.Library.BusinessLogic;
using Org.Eclipse.TractusX.Portal.Backend.Framework.Async;
using Org.Eclipse.TractusX.Portal.Backend.Framework.DateTimeProvider;
using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling;
using Org.Eclipse.TractusX.Portal.Backend.Framework.Linq;
using Org.Eclipse.TractusX.Portal.Backend.Framework.Processes.Library.Enums;
using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.DBAccess;
using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.DBAccess.Extensions;
using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.DBAccess.Repositories;
using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.PortalEntities.Enums;
using Org.Eclipse.TractusX.Portal.Backend.Processes.ApplicationChecklist.Library;
using System.Collections.Immutable;

namespace Org.Eclipse.TractusX.Portal.Backend.Clearinghouse.Library.BusinessLogic;

public class ClearinghouseBusinessLogic(
IPortalRepositories portalRepositories,
IClearinghouseService clearinghouseService,
ICustodianBusinessLogic custodianBusinessLogic,
IApplicationChecklistService checklistService,
IDateTimeProvider dateTimeProvider,
IOptions<ClearinghouseSettings> options)
Expand All @@ -44,37 +45,14 @@ public class ClearinghouseBusinessLogic(

public async Task<IApplicationChecklistService.WorkerChecklistProcessStepExecutionResult> HandleClearinghouse(IApplicationChecklistService.WorkerChecklistProcessStepData context, CancellationToken cancellationToken)
{
var overwrite = context.ProcessStepTypeId switch
var validationMode = context.ProcessStepTypeId switch
{
ProcessStepTypeId.START_OVERRIDE_CLEARING_HOUSE => true,
ProcessStepTypeId.START_CLEARING_HOUSE => false,
ProcessStepTypeId.START_OVERRIDE_CLEARING_HOUSE => ValidationModes.IDENTIFIER,
ProcessStepTypeId.START_CLEARING_HOUSE => ValidationModes.LEGAL_NAME,
_ => throw new UnexpectedConditionException($"HandleClearingHouse called for unexpected processStepTypeId {context.ProcessStepTypeId}. Expected {ProcessStepTypeId.START_CLEARING_HOUSE} or {ProcessStepTypeId.START_OVERRIDE_CLEARING_HOUSE}")
};

string companyDid;
if (_settings.UseDimWallet)
{
var (exists, did) = await portalRepositories.GetInstance<IApplicationRepository>()
.GetDidForApplicationId(context.ApplicationId).ConfigureAwait(ConfigureAwaitOptions.None);
if (!exists || string.IsNullOrWhiteSpace(did))
{
throw new ConflictException($"Did must be set for Application {context.ApplicationId}");
}

companyDid = did;
}
else
{
var walletData = await custodianBusinessLogic.GetWalletByBpnAsync(context.ApplicationId, cancellationToken);
if (walletData == null || string.IsNullOrEmpty(walletData.Did))
{
throw new ConflictException($"Decentralized Identifier for application {context.ApplicationId} is not set");
}

companyDid = walletData.Did;
}

await TriggerCompanyDataPost(context.ApplicationId, companyDid, overwrite, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None);
await TriggerCompanyDataPost(context.ApplicationId, validationMode, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None);

return new IApplicationChecklistService.WorkerChecklistProcessStepExecutionResult(
ProcessStepStatusId.DONE,
Expand All @@ -85,7 +63,7 @@ public class ClearinghouseBusinessLogic(
null);
}

private async Task TriggerCompanyDataPost(Guid applicationId, string decentralizedIdentifier, bool overwrite, CancellationToken cancellationToken)
private async Task TriggerCompanyDataPost(Guid applicationId, string validationMode, CancellationToken cancellationToken)
{
var data = await portalRepositories.GetInstance<IApplicationRepository>()
.GetClearinghouseDataForApplicationId(applicationId).ConfigureAwait(ConfigureAwaitOptions.None);
Expand All @@ -99,16 +77,33 @@ private async Task TriggerCompanyDataPost(Guid applicationId, string decentraliz
throw new ConflictException($"CompanyApplication {applicationId} is not in status SUBMITTED");
}

if (string.IsNullOrWhiteSpace(data.ParticipantDetails.Bpn))
if (string.IsNullOrWhiteSpace(data.Bpn))
{
throw new ConflictException("BusinessPartnerNumber is null");
}

if (data.Address is null)
{
throw new ConflictException($"Address does not exist.");
}

var headers = ImmutableDictionary.CreateRange<string, string>([new("Business-Partner-Number", data.Bpn)]);

var transferData = new ClearinghouseTransferData(
data.ParticipantDetails,
new IdentityDetails(decentralizedIdentifier, data.UniqueIds),
_settings.CallbackUrl,
overwrite);
new LegalEntity(
data.Name,
new LegalAddress(
data.Address.CountryAlpha2Code,
string.Format("{0}-{1}", data.Address.CountryAlpha2Code, data.Address.Region),
data.Address.City,
data.Address.Zipcode,
string.IsNullOrEmpty(data.Address.Streetnumber)
? data.Address.Streetname
: string.Format("{0} {1}", data.Address.Streetname, data.Address.Streetnumber)),
data.Identifiers.Select(ci => new UniqueIdData(ci.UniqueIdentifierId.GetUniqueIdentifierValue(), ci.Value))),
validationMode,
new CallBack(_settings.CallbackUrl, headers)
);

await clearinghouseService.TriggerCompanyDataPost(transferData, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None);
}
Expand All @@ -124,19 +119,24 @@ public async Task ProcessEndClearinghouse(Guid applicationId, ClearinghouseRespo
processStepTypeIds: [ProcessStepTypeId.START_SELF_DESCRIPTION_LP])
.ConfigureAwait(ConfigureAwaitOptions.None);

var declined = data.Status == ClearinghouseResponseStatus.DECLINE;

// Company data is valid if any one of the provided identifiers was responded valid from CH
var validData = data.ValidationUnits.FirstOrDefault(s => s.Status == ClearinghouseResponseStatus.VALID);
var isInvalid = validData == null;
checklistService.FinalizeChecklistEntryAndProcessSteps(
context,
null,
item =>
{
item.ApplicationChecklistEntryStatusId = declined
item.ApplicationChecklistEntryStatusId = isInvalid
? ApplicationChecklistEntryStatusId.FAILED
: ApplicationChecklistEntryStatusId.DONE;
item.Comment = data.Message;

// There is not "Message" param available in the response in case of VALID so, thats why saving ClearinghouseResponseStatus param into the Comments in case of VALID only.
item.Comment = isInvalid
? string.Join(", ", data.ValidationUnits.Where(s => s.Status != ClearinghouseResponseStatus.VALID).Select(x => x.Reason?.DetailMessage))
: validData!.Status.ToString();
},
declined
isInvalid
? [ProcessStepTypeId.MANUAL_TRIGGER_OVERRIDE_CLEARING_HOUSE]
: [ProcessStepTypeId.START_SELF_DESCRIPTION_LP]);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public async Task TriggerCompanyDataPost(ClearinghouseTransferData data, Cancell
async ValueTask<(bool, string?)> CreateErrorMessage(HttpResponseMessage errorResponse) =>
(false, (await errorResponse.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None)));

await httpClient.PostAsJsonAsync("/api/v1/validation", data, cancellationToken)
await httpClient.PostAsJsonAsync("/api/v2/validation", data, cancellationToken)
.CatchingIntoServiceExceptionFor("clearinghouse-post", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE, CreateErrorMessage).ConfigureAwait(false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,17 @@
namespace Org.Eclipse.TractusX.Portal.Backend.Clearinghouse.Library.Models;

public record ClearinghouseResponseData(
[property: JsonPropertyName("bpn")] string BusinessPartnerNumber,
[property: JsonPropertyName("status")] ClearinghouseResponseStatus Status,
[property: JsonPropertyName("message")] string? Message);
[property: JsonPropertyName("validationMode")] string ValidationMode,
[property: JsonPropertyName("validationUnits")] IEnumerable<ValidationUnits> ValidationUnits
);

public record ValidationUnits(
[property: JsonPropertyName("result")] ClearinghouseResponseStatus Status,
[property: JsonPropertyName("type")] string Type,
[property: JsonPropertyName("reason")] Reason? Reason
);

public record Reason(
[property: JsonPropertyName("message")] string Message,
[property: JsonPropertyName("detailMessage")] string DetailMessage
);
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,18 @@ namespace Org.Eclipse.TractusX.Portal.Backend.Clearinghouse.Library.Models;

public enum ClearinghouseResponseStatus
{
CONFIRM = 1,
/// <summary>
/// In case the identifier has been found in the trust sources of clearing house.
/// </summary>
VALID = 1,

DECLINE = 2
/// <summary>
/// In case the identifier format is not valid or the identifier was not found in the trust source of clearing house.
/// </summary>
INVALID = 2,

/// <summary>
/// In case the validation can't be performed, due to the unavailablity of the trust source of clearing house.
/// </summary>
INCONCLUSIVE = 3
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
/********************************************************************************
* Copyright (c) 2022 Microsoft and BMW Group AG
* Copyright (c) 2022 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
Expand All @@ -18,17 +17,36 @@
* SPDX-License-Identifier: Apache-2.0
********************************************************************************/

using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.DBAccess.Models;
using System.Text.Json.Serialization;

namespace Org.Eclipse.TractusX.Portal.Backend.Clearinghouse.Library.Models;

public record ClearinghouseTransferData(
[property: JsonPropertyName("participantDetails")] ParticipantDetails ParticipantDetails,
[property: JsonPropertyName("identityDetails")] IdentityDetails IdentityDetails,
[property: JsonPropertyName("callbackUrl")] string CallbackUrl,
[property: JsonPropertyName("exceptProfile")] bool ExceptProfile);
[property: JsonPropertyName("legalEntity")] LegalEntity LegalEntity,
[property: JsonPropertyName("validationMode")] string ValidationMode,
[property: JsonPropertyName("callback")] CallBack Callback
);

public record IdentityDetails(
[property: JsonPropertyName("did")] string Did,
[property: JsonPropertyName("uniqueIds")] IEnumerable<UniqueIdData> UniqueIds);
public record LegalEntity(
[property: JsonPropertyName("legalName")] string LegalName,
[property: JsonPropertyName("address")] LegalAddress Address,
[property: JsonPropertyName("identifiers")] IEnumerable<UniqueIdData> Identifiers
);

public record LegalAddress(
[property: JsonPropertyName("country")] string CountryAlpha2Code,
[property: JsonPropertyName("region")] string Region,
[property: JsonPropertyName("locality")] string City,
[property: JsonPropertyName("postalCode")] string? ZipCode,
[property: JsonPropertyName("addressLine")] string AddressLine
);

public record UniqueIdData(
[property: JsonPropertyName("type")] string Type,
[property: JsonPropertyName("value")] string Value
);

public record CallBack(
[property: JsonPropertyName("url")] string Url,
[property: JsonPropertyName("headers")] IReadOnlyDictionary<string, string> Headers
);
36 changes: 36 additions & 0 deletions src/externalsystems/Clearinghouse.Library/ValidationModes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/********************************************************************************
* Copyright (c) 2024 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* SPDX-License-Identifier: Apache-2.0
********************************************************************************/

namespace Org.Eclipse.TractusX.Portal.Backend.Clearinghouse.Library;

public static class ValidationModes
{
/// <summary>
/// DEFAULT - validates whether the identifiers themselves exists, indepenedent of their relationship to the legal entity provided
/// </summary>
public const string IDENTIFIER = "IDENTIFIER";
/// <summary>
/// Validates whether the identifier is valid, and whether the name of the legal entity it is associated with matches the provided legal name
/// </summary>
public const string LEGAL_NAME = "LEGAL_NAME";
/// <summary>
/// Validates whether the identifier is valid, and whether the name of the legal entity, as well as the addresss it is associated with matches the provided ones.
/// </summary>
public const string LEGAL_NAME_AND_ADDRESS = "LEGAL_NAME_AND_ADDRESS";
}
Loading

0 comments on commit 54fbbe2

Please sign in to comment.