Skip to content

Commit

Permalink
feat(app&service): enable provider to decline subscription request (#…
Browse files Browse the repository at this point in the history
…1171)

* added /api/apps/subscription/{subscriptionId}/decline
* added /api/services/subscription/{subscriptionId}/decline

------

Refs: #1140
Reviewed-by: Phil Schneider <[email protected]>
  • Loading branch information
tfjanjua authored Jan 16, 2025
1 parent 2fafec7 commit a79ee6b
Show file tree
Hide file tree
Showing 27 changed files with 11,532 additions and 4 deletions.
46 changes: 46 additions & 0 deletions docs/api/apps-service.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1838,6 +1838,52 @@ paths:
$ref: '#/components/schemas/ErrorResponse'
'401':
description: The User is unauthorized
'/api/apps/subscription/{subscriptionId}/decline':
put:
tags:
- Apps
summary: 'Declines a pending app subscription. (Authorization required - Roles: decline_subscription)'
description: 'Example: PUT: /api/apps/subscription/{subscriptiondId}/decline'
parameters:
- name: subscriptionId
in: path
description: ID of the subscription to decline.
required: true
schema:
type: string
format: uuid
example: D3B1ECA2-6148-4008-9E6C-C1C2AEA5C645
responses:
'204':
description: App subscription was successfully declined.
'404':
description: 'If sub claim is empty/invalid or user does not exist, or any other parameters are invalid.'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'403':
description: Forbidden
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'409':
description: Conflict
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'500':
description: Internal Server Error.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'400':
description: 'If sub claim is empty/invalid or user does not exist, or any other parameters are invalid.'
'401':
description: The User is unauthorized
'/api/apps/{subscriptionId}/unsubscribe':
put:
tags:
Expand Down
2 changes: 2 additions & 0 deletions docs/api/notifications-service.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,8 @@ components:
- CREDENTIAL_APPROVAL
- CREDENTIAL_REJECTED
- CREDENTIAL_EXPIRY
- APP_SUBSCRIPTION_DECLINE
- SERVICE_SUBSCRIPTION_DECLINE
type: string
SearchSemanticTypeId:
enum:
Expand Down
46 changes: 46 additions & 0 deletions docs/api/services-service.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1514,6 +1514,52 @@ paths:
$ref: '#/components/schemas/ErrorResponse'
'401':
description: The User is unauthorized
'/subscription/{subscriptionId}/decline':
put:
tags:
- Services
summary: 'Declines a pending service subscription. (Authorization required - Roles: decline_subscription)'
description: 'Example: PUT: /api/services/supscription/{subscriptiondId}/decline'
parameters:
- name: subscriptionId
in: path
description: ID of the subscription to decline.
required: true
schema:
type: string
format: uuid
example: D3B1ECA2-6148-4008-9E6C-C1C2AEA5C645
responses:
'204':
description: Service subscription was successfully declined.
'404':
description: Not Found
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'403':
description: Forbidden
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'409':
description: Conflict
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'500':
description: Internal Server Error.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'400':
description: 'If sub claim is empty/invalid or user does not exist, or any other parameters are invalid.'
'401':
description: The User is unauthorized
components:
schemas:
AgreementConsentStatus:
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions src/mailing/Mailing.Template/Enums/EmailTemplateType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ public enum EmailTemplateType
[Path("appprovider_subscription_request.html")]
AppSubscriptionRequest,

/// <summary>
/// Email template for notifying requester (subscriber) of subscription declination.
/// </summary>
[Path("app_subscription_decline.html")]
AppSubscriptionDecline,

/// <summary>
/// Email template for notifying app providers of subscription activition.
/// </summary>
Expand All @@ -95,6 +101,12 @@ public enum EmailTemplateType
[Path("serviceprovider_subscription_request.html")]
ServiceSubscriptionRequest,

/// <summary>
/// Email template for notifying requester (subscriber) of subscription declination.
/// </summary>
[Path("service_subscription_decline.html")]
ServiceSubscriptionDecline,

/// <summary>
/// Email template for notifying requester of subscription activations.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,10 @@ public async Task AddFavouriteAppForUserAsync(Guid appId)
public Task<Guid> AddOwnCompanyAppSubscriptionAsync(Guid appId, IEnumerable<OfferAgreementConsentData> offerAgreementConsentData) =>
_offerSubscriptionService.AddOfferSubscriptionAsync(appId, offerAgreementConsentData, OfferTypeId.APP, _settings.BasePortalAddress, _settings.SubscriptionManagerRoles, _settings.ServiceManagerRoles);

/// <inheritdoc/>
public Task DeclineAppSubscriptionAsync(Guid subscriptionId) =>
_offerSubscriptionService.RemoveOfferSubscriptionAsync(subscriptionId, OfferTypeId.APP, _settings.BasePortalAddress);

/// <inheritdoc/>
public Task TriggerActivateOfferSubscription(Guid subscriptionId) =>
_offerSetupService.TriggerActivateSubscription(subscriptionId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,12 @@ public interface IAppsBusinessLogic
/// <param name="offerAgreementConsentData">The agreement consent data</param>
public Task<Guid> AddOwnCompanyAppSubscriptionAsync(Guid appId, IEnumerable<OfferAgreementConsentData> offerAgreementConsentData);

/// <summary>
/// Declines a pending app subscription of an app, provided by the current user's company.
/// </summary>
/// <param name="subscriptionId">ID of the pending app to be declined.</param>
public Task DeclineAppSubscriptionAsync(Guid subscriptionId);

/// <summary>
/// Activates a pending app subscription for an app provided by the current user's company.
/// </summary>
Expand Down
25 changes: 25 additions & 0 deletions src/marketplace/Apps.Service/Controllers/AppsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,31 @@ public async Task<IActionResult> ActivateOfferSubscription([FromRoute] Guid subs
return NoContent();
}

/// <summary>
/// Declines a pending app subscription.
/// </summary>
/// <param name="subscriptionId" example="D3B1ECA2-6148-4008-9E6C-C1C2AEA5C645">ID of the subscription to decline.</param>
/// <remarks>Example: PUT: /api/apps/subscription/{subscriptiondId}/decline</remarks>
/// <response code="204">App subscription was successfully declined.</response>
/// <response code="400">If sub claim is empty/invalid or user does not exist, or any other parameters are invalid.</response>
/// <response code="404">If sub claim is empty/invalid or user does not exist, or any other parameters are invalid.</response>
/// <response code="500">Internal Server Error.</response>
[HttpPut]
[Route("subscription/{subscriptionId}/decline")]
[Authorize(Roles = "decline_subscription")]
[Authorize(Policy = PolicyTypes.ValidIdentity)]
[Authorize(Policy = PolicyTypes.ValidCompany)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status403Forbidden)]
[ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status409Conflict)]
[ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> DeclineAppSubscriptionAsync([FromRoute] Guid subscriptionId)
{
await _appsBusinessLogic.DeclineAppSubscriptionAsync(subscriptionId).ConfigureAwait(ConfigureAwaitOptions.None);
return NoContent();
}

/// <summary>
/// Unsubscribes an app from the current user's company's subscriptions.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/********************************************************************************
* 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
********************************************************************************/

using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling.Service;
using System.Collections.Immutable;

namespace Org.Eclipse.TractusX.Portal.Backend.Offers.Library.ErrorHandling;

public class OfferSubscriptionServiceErrorMessageContainer : IErrorMessageContainer
{
private static readonly IReadOnlyDictionary<int, string> _messageContainer = new Dictionary<OfferSubscriptionServiceErrors, string> {
{ OfferSubscriptionServiceErrors.OFFER_NOTFOUND, "Offer {offerId} does not exist." },
{ OfferSubscriptionServiceErrors.SUBSCRIPTION_NOTFOUND, "Subscription {subscriptionId} does not exist." },
{ OfferSubscriptionServiceErrors.NON_PROVIDER_IS_FORBIDDEN, "Only the providing company can decline the subscription request." },
{ OfferSubscriptionServiceErrors.OFFER_STATUS_CONFLICT_INCORR_OFFER_STATUS, "Subscription of offer {offerName} should be in {offerStatus} state." },
}.ToImmutableDictionary(x => (int)x.Key, x => x.Value);

public Type Type { get => typeof(OfferSubscriptionServiceErrors); }
public IReadOnlyDictionary<int, string> MessageContainer { get => _messageContainer; }
}

public enum OfferSubscriptionServiceErrors
{
OFFER_NOTFOUND,
SUBSCRIPTION_NOTFOUND,
NON_PROVIDER_IS_FORBIDDEN,
OFFER_STATUS_CONFLICT_INCORR_OFFER_STATUS
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@ namespace Org.Eclipse.TractusX.Portal.Backend.Offers.Library.Service;
public interface IOfferSubscriptionService
{
Task<Guid> AddOfferSubscriptionAsync(Guid offerId, IEnumerable<OfferAgreementConsentData> offerAgreementConsentData, OfferTypeId offerTypeId, string basePortalAddress, IEnumerable<UserRoleConfig> notificationRecipients, IEnumerable<UserRoleConfig> serviceManagerRoles);
Task<Guid> RemoveOfferSubscriptionAsync(Guid subscriptionId, OfferTypeId offerTypeId, string basePortalAddress);
}
62 changes: 62 additions & 0 deletions src/marketplace/Offers.Library/Service/OfferSubscriptionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling;
using Org.Eclipse.TractusX.Portal.Backend.Framework.Linq;
using Org.Eclipse.TractusX.Portal.Backend.Framework.Models.Configuration;
using Org.Eclipse.TractusX.Portal.Backend.Offers.Library.ErrorHandling;
using Org.Eclipse.TractusX.Portal.Backend.Offers.Library.Models;
using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.DBAccess;
using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.DBAccess.Models;
Expand Down Expand Up @@ -103,6 +104,67 @@ await _mailingProcessCreation.RoleBaseSendMail(
return offerSubscription.Id;
}

public async Task<Guid> RemoveOfferSubscriptionAsync(Guid subscriptionId, OfferTypeId offerTypeId, string basePortalAddress)
{
var offerSubscriptionDetails = await _portalRepositories.GetInstance<IOfferSubscriptionsRepository>()
.GetOfferDetailsAndCheckProviderCompany(subscriptionId, _identityData.CompanyId, offerTypeId) ?? throw NotFoundException.Create(OfferSubscriptionServiceErrors.SUBSCRIPTION_NOTFOUND, new ErrorParameter[] { new("subscriptionId", subscriptionId.ToString()) });
var offerId = offerSubscriptionDetails.OfferId;

if (string.IsNullOrEmpty(offerSubscriptionDetails.OfferName))
{
throw NotFoundException.Create(OfferSubscriptionServiceErrors.OFFER_NOTFOUND, new ErrorParameter[] { new("offerId", offerId.ToString()) });
}
if (!offerSubscriptionDetails.IsProviderCompany)
{
throw ForbiddenException.Create(OfferSubscriptionServiceErrors.NON_PROVIDER_IS_FORBIDDEN);
}
if (offerSubscriptionDetails.Status != OfferSubscriptionStatusId.PENDING)
{
throw ConflictException.Create(OfferSubscriptionServiceErrors.OFFER_STATUS_CONFLICT_INCORR_OFFER_STATUS, new ErrorParameter[] { new("offerName", offerSubscriptionDetails.OfferName), new("offerStatus", OfferSubscriptionStatusId.PENDING.ToString()) });
}

var offerSubscription = _portalRepositories.Remove(new OfferSubscription(subscriptionId, offerId, offerSubscriptionDetails.CompanyId, offerSubscriptionDetails.Status, offerSubscriptionDetails.RequesterId, DateTimeOffset.UtcNow));
SendNotificationsToRequester(offerId, offerTypeId, basePortalAddress, offerSubscriptionDetails);
await _portalRepositories.SaveAsync().ConfigureAwait(ConfigureAwaitOptions.None);

return offerSubscription.Id;
}

private void SendNotificationsToRequester(Guid offerId, OfferTypeId offerTypeId, string basePortalAddress, OfferSubscriptionTransferData offerSubscriptionDetails)
{
var content = JsonSerializer.Serialize(new
{
AppName = offerSubscriptionDetails.OfferName,
OfferId = offerId
});

var notificationTypeId = offerTypeId == OfferTypeId.SERVICE ? NotificationTypeId.SERVICE_SUBSCRIPTION_DECLINE : NotificationTypeId.APP_SUBSCRIPTION_DECLINE;
_portalRepositories.GetInstance<INotificationRepository>().CreateNotification(
offerSubscriptionDetails.RequesterId,
notificationTypeId,
false,
notification =>
{
notification.CreatorUserId = _identityData.IdentityId;
notification.Content = content;
});

if (!string.IsNullOrWhiteSpace(offerSubscriptionDetails.RequesterEmail))
{
var mailParameters = ImmutableDictionary.CreateRange(
[
KeyValuePair.Create("offerName", offerSubscriptionDetails.OfferName!),
KeyValuePair.Create("url", basePortalAddress),
KeyValuePair.Create("requesterName", string.Format("{0} {1}", offerSubscriptionDetails.RequesterFirstname, offerSubscriptionDetails.RequesterLastname))
]);

_mailingProcessCreation.CreateMailProcess(
offerSubscriptionDetails.RequesterEmail,
$"{offerTypeId.ToString().ToLower()}-subscription-decline",
mailParameters);
}
}

private void CreateProcessSteps(OfferSubscription offerSubscription)
{
var processStepRepository = _portalRepositories.GetInstance<IProcessStepRepository>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ public interface IServiceBusinessLogic
/// <returns></returns>
Task<Guid> AddServiceSubscription(Guid serviceId, IEnumerable<OfferAgreementConsentData> offerAgreementConsentData);

/// <summary>
/// Declines a pending service subscription of a service, provided by the current user's company.
/// </summary>
/// <param name="subscriptionId">ID of the pending service to be declined.</param>
public Task DeclineServiceSubscriptionAsync(Guid subscriptionId);

/// <summary>
/// Gets the service detail data for the given service
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ public ServiceBusinessLogic(
public Task<Guid> AddServiceSubscription(Guid serviceId, IEnumerable<OfferAgreementConsentData> offerAgreementConsentData) =>
_offerSubscriptionService.AddOfferSubscriptionAsync(serviceId, offerAgreementConsentData, OfferTypeId.SERVICE, _settings.BasePortalAddress, _settings.SubscriptionManagerRoles, _settings.ServiceManagerRoles);

/// <inheritdoc />
public Task DeclineServiceSubscriptionAsync(Guid subscriptionId) =>
_offerSubscriptionService.RemoveOfferSubscriptionAsync(subscriptionId, OfferTypeId.SERVICE, _settings.BasePortalAddress);

/// <inheritdoc />
public async Task<ServiceDetailResponse> GetServiceDetailsAsync(Guid serviceId, string lang)
{
Expand Down
24 changes: 24 additions & 0 deletions src/marketplace/Services.Service/Controllers/ServicesController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -354,4 +354,28 @@ public async Task<IActionResult> ActivateCompanyAppSubscriptionAsync([FromRoute]
await _serviceBusinessLogic.TriggerActivateOfferSubscription(subscriptionId).ConfigureAwait(ConfigureAwaitOptions.None);
return NoContent();
}

/// <summary>
/// Declines a pending service subscription.
/// </summary>
/// <param name="subscriptionId" example="D3B1ECA2-6148-4008-9E6C-C1C2AEA5C645">ID of the subscription to decline.</param>
/// <remarks>Example: PUT: /api/services/supscription/{subscriptiondId}/decline</remarks>
/// <response code="204">Service subscription was successfully declined.</response>
/// <response code="400">If sub claim is empty/invalid or user does not exist, or any other parameters are invalid.</response>
/// <response code="500">Internal Server Error.</response>
[HttpPut]
[Route("/subscription/{subscriptionId}/decline")]
[Authorize(Roles = "decline_subscription")]
[Authorize(Policy = PolicyTypes.ValidIdentity)]
[Authorize(Policy = PolicyTypes.ValidCompany)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status403Forbidden)]
[ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status409Conflict)]
[ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> DeclineServiceSubscriptionAsync([FromRoute] Guid subscriptionId)
{
await _serviceBusinessLogic.DeclineServiceSubscriptionAsync(subscriptionId).ConfigureAwait(ConfigureAwaitOptions.None);
return NoContent();
}
}
Loading

0 comments on commit a79ee6b

Please sign in to comment.