Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(app&service): enable provider to decline subscription request #1171

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading