Browse Source

App plan handling changed.

pull/95/head
Sebastian Stehle 9 years ago
parent
commit
6860645e40
  1. 2
      src/Squidex.Domain.Apps.Read/Apps/Services/IAppPlanBillingManager.cs
  2. 14
      src/Squidex.Domain.Apps.Read/Apps/Services/IChangePlanResult.cs
  3. 4
      src/Squidex.Domain.Apps.Read/Apps/Services/Implementations/NoopAppPlanBillingManager.cs
  4. 18
      src/Squidex.Domain.Apps.Read/Apps/Services/PlanChangeAsyncResult.cs
  5. 19
      src/Squidex.Domain.Apps.Read/Apps/Services/PlanChangedResult.cs
  6. 25
      src/Squidex.Domain.Apps.Read/Apps/Services/RedirectToCheckoutResult.cs
  7. 16
      src/Squidex.Domain.Apps.Write/Apps/AppCommandHandler.cs
  8. 2
      src/Squidex.Domain.Apps.Write/Apps/Commands/ChangePlan.cs
  9. 2
      src/Squidex.Infrastructure.GetEventStore/CQRS/Events/GetEventStoreSubscription.cs
  10. 2
      src/Squidex.Infrastructure.MongoDb/CQRS/Events/MongoEventStore.cs
  11. 23
      src/Squidex.Infrastructure/CQRS/Events/EventReceiver.cs
  12. 37
      src/Squidex.Infrastructure/Timers/CompletionTimer.cs
  13. 26
      src/Squidex/Config/Domain/ReadModule.cs
  14. 14
      src/Squidex/Controllers/Api/Plans/AppPlansController.cs
  15. 18
      src/Squidex/Controllers/Api/Plans/Models/PlanChangedDto.cs
  16. 18
      src/Squidex/app/features/settings/pages/plans/plans-page.component.ts
  17. 11
      src/Squidex/app/shared/services/plans.service.spec.ts
  18. 12
      src/Squidex/app/shared/services/plans.service.ts
  19. 40
      tests/Squidex.Domain.Apps.Write.Tests/Apps/AppCommandHandlerTests.cs
  20. 13
      tests/Squidex.Infrastructure.Tests/CQRS/Events/EventReceiverTests.cs
  21. 20
      tests/Squidex.Infrastructure.Tests/Timers/CompletionTimerTests.cs

2
src/Squidex.Domain.Apps.Read/Apps/Services/IAppPlanBillingManager.cs

@ -15,7 +15,7 @@ namespace Squidex.Domain.Apps.Read.Apps.Services
{
bool HasPortal { get; }
Task ChangePlanAsync(string userId, Guid appId, string appName, string planId);
Task<IChangePlanResult> ChangePlanAsync(string userId, Guid appId, string appName, string planId);
Task<bool> HasPaymentOptionsAsync(string userId);

14
src/Squidex.Domain.Apps.Read/Apps/Services/IChangePlanResult.cs

@ -0,0 +1,14 @@
// ==========================================================================
// IChangePlanResult.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Read.Apps.Services
{
public interface IChangePlanResult
{
}
}

4
src/Squidex.Domain.Apps.Read/Apps/Services/Implementations/NoopAppPlanBillingManager.cs

@ -19,9 +19,9 @@ namespace Squidex.Domain.Apps.Read.Apps.Services.Implementations
get { return false; }
}
public Task ChangePlanAsync(string userId, Guid appId, string appName, string planId)
public Task<IChangePlanResult> ChangePlanAsync(string userId, Guid appId, string appName, string planId)
{
return TaskHelper.Done;
return Task.FromResult<IChangePlanResult>(PlanChangedResult.Instance);
}
public Task<bool> HasPaymentOptionsAsync(string userId)

18
src/Squidex.Domain.Apps.Read/Apps/Services/PlanChangeAsyncResult.cs

@ -0,0 +1,18 @@
// ==========================================================================
// PlanChangeAsyncResult.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Read.Apps.Services
{
public sealed class PlanChangeAsyncResult : IChangePlanResult
{
public static readonly PlanChangeAsyncResult Instance = new PlanChangeAsyncResult();
private PlanChangeAsyncResult()
{
}
}
}

19
src/Squidex.Domain.Apps.Read/Apps/Services/PlanChangedResult.cs

@ -0,0 +1,19 @@
// ==========================================================================
// PlanChangedResult.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Read.Apps.Services
{
public sealed class PlanChangedResult : IChangePlanResult
{
public static readonly PlanChangedResult Instance = new PlanChangedResult();
private PlanChangedResult()
{
}
}
}

25
src/Squidex.Domain.Apps.Read/Apps/Services/RedirectToCheckoutResult.cs

@ -0,0 +1,25 @@
// ==========================================================================
// RedirectToCheckoutResult.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Read.Apps.Services
{
public sealed class RedirectToCheckoutResult : IChangePlanResult
{
public Uri Url { get; }
public RedirectToCheckoutResult(Uri url)
{
Guard.NotNull(url, nameof(url));
Url = url;
}
}
}

16
src/Squidex.Domain.Apps.Write/Apps/AppCommandHandler.cs

@ -107,9 +107,21 @@ namespace Squidex.Domain.Apps.Write.Apps
return handler.UpdateAsync<AppDomainObject>(context, async a =>
{
a.ChangePlan(command);
if (command.FromCallback)
{
a.ChangePlan(command);
}
else
{
var result = await appPlansBillingManager.ChangePlanAsync(command.Actor.Identifier, a.Id, a.Name, command.PlanId);
await appPlansBillingManager.ChangePlanAsync(command.Actor.Identifier, a.Id, a.Name, command.PlanId);
if (result is PlanChangedResult)
{
a.ChangePlan(command);
}
context.Succeed(result);
}
});
}

2
src/Squidex.Domain.Apps.Write/Apps/Commands/ChangePlan.cs

@ -13,6 +13,8 @@ namespace Squidex.Domain.Apps.Write.Apps.Commands
{
public sealed class ChangePlan : AppAggregateCommand, IValidatable
{
public bool FromCallback { get; set; }
public string PlanId { get; set; }
public void Validate(IList<ValidationError> errors)

2
src/Squidex.Infrastructure.GetEventStore/CQRS/Events/GetEventStoreSubscription.cs

@ -189,7 +189,7 @@ namespace Squidex.Infrastructure.CQRS.Events
}
}
if (reason != SubscriptionDropReason.UserInitiated && reason != SubscriptionDropReason.EventHandlerException)
if (reason != SubscriptionDropReason.UserInitiated)
{
var exception = ex ?? new ConnectionClosedException($"Subscription closed with reason {reason}.");

2
src/Squidex.Infrastructure.MongoDb/CQRS/Events/MongoEventStore.cs

@ -83,7 +83,7 @@ namespace Squidex.Infrastructure.CQRS.Events
var filter = CreateFilter(streamFilter, lastPosition);
await Collection.Find(filter).Sort(Sort.Ascending(EventStreamField)).ForEachAsync(async commit =>
await Collection.Find(filter).Sort(Sort.Ascending(TimestampField)).ForEachAsync(async commit =>
{
var eventStreamOffset = (int)commit.EventStreamOffset;

23
src/Squidex.Infrastructure/CQRS/Events/EventReceiver.cs

@ -140,26 +140,17 @@ namespace Squidex.Infrastructure.CQRS.Events
var subscription = eventStore.CreateSubscription(eventConsumer.EventsFilter, position);
async Task StopSubscriptionAsync(Exception exception)
await subscription.SubscribeAsync(async storedEvent =>
{
await eventConsumerInfoRepository.StopAsync(consumerName, exception.ToString());
subscription.Dispose();
}
await DispatchConsumer(ParseEvent(storedEvent), eventConsumer, eventConsumer.Name);
await subscription.SubscribeAsync(async storedEvent =>
await eventConsumerInfoRepository.SetPositionAsync(eventConsumer.Name, storedEvent.EventPosition, false);
}, async exception =>
{
try
{
await DispatchConsumer(ParseEvent(storedEvent), eventConsumer, eventConsumer.Name);
await eventConsumerInfoRepository.StopAsync(consumerName, exception.ToString());
await eventConsumerInfoRepository.SetPositionAsync(eventConsumer.Name, storedEvent.EventPosition, false);
}
catch (Exception ex)
{
await StopSubscriptionAsync(ex);
}
}, StopSubscriptionAsync);
subscription.Dispose();
});
currentSubscription = subscription;
}

37
src/Squidex.Infrastructure/Timers/CompletionTimer.cs

@ -50,26 +50,33 @@ namespace Squidex.Infrastructure.Timers
private async Task RunInternal(int delay, int initialDelay, Func<CancellationToken, Task> callback)
{
if (initialDelay > 0)
{
await WaitAsync(initialDelay).ConfigureAwait(false);
}
while (requiresAtLeastOne == 2 || !disposeToken.IsCancellationRequested)
try
{
try
{
await callback(disposeToken.Token).ConfigureAwait(false);
}
catch (OperationCanceledException)
if (initialDelay > 0)
{
await WaitAsync(initialDelay).ConfigureAwait(false);
}
finally
while (requiresAtLeastOne == 2 || !disposeToken.IsCancellationRequested)
{
requiresAtLeastOne = 1;
}
try
{
await callback(disposeToken.Token).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
}
finally
{
requiresAtLeastOne = 1;
}
await WaitAsync(delay).ConfigureAwait(false);
await WaitAsync(delay).ConfigureAwait(false);
}
}
catch
{
}
}

26
src/Squidex/Config/Domain/ReadModule.cs

@ -9,8 +9,10 @@
using System.Collections.Generic;
using System.Linq;
using Autofac;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using Squidex.Chargebee;
using Squidex.Domain.Apps.Read.Apps;
using Squidex.Domain.Apps.Read.Apps.Services;
using Squidex.Domain.Apps.Read.Apps.Services.Implementations;
@ -22,8 +24,10 @@ using Squidex.Domain.Apps.Read.Schemas;
using Squidex.Domain.Apps.Read.Schemas.Services;
using Squidex.Domain.Apps.Read.Schemas.Services.Implementations;
using Squidex.Domain.Users;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Pipeline;
using Squidex.Shared.Users;
// ReSharper disable UnusedAutoPropertyAccessor.Local
@ -55,11 +59,6 @@ namespace Squidex.Config.Domain
.AsSelf()
.SingleInstance();
builder.RegisterType<NoopAppPlanBillingManager>()
.As<IAppPlanBillingManager>()
.AsSelf()
.SingleInstance();
builder.RegisterType<CachingSchemaProvider>()
.As<ISchemaProvider>()
.AsSelf()
@ -98,6 +97,23 @@ namespace Squidex.Config.Domain
.As<IGraphQLService>()
.AsSelf()
.InstancePerDependency();
var chargebeeSiteName = Configuration.GetValue<string>("chargebee:siteName");
var chargebeeApiKey = Configuration.GetValue<string>("chargebee:apiKey");
if (string.IsNullOrWhiteSpace(chargebeeSiteName))
{
throw new ConfigurationException("Configure Chargebee SiteName type with 'chargebee:siteName'.");
}
if (string.IsNullOrWhiteSpace(chargebeeApiKey))
{
throw new ConfigurationException("Configure Chargebee ApiKey with 'chargebee:apiKey'.");
}
builder.Register(c => new ChargebeeAppPlanBillingManager(c.Resolve<UserManager<IUser>>(), chargebeeSiteName, chargebeeApiKey))
.As<IAppPlanBillingManager>()
.AsSelf()
.SingleInstance();
}
}
}

14
src/Squidex/Controllers/Api/Plans/AppPlansController.cs

@ -19,6 +19,8 @@ using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.Security;
using Squidex.Pipeline;
// ReSharper disable RedundantIfElseBlock
namespace Squidex.Controllers.Api.Plans
{
/// <summary>
@ -81,6 +83,7 @@ namespace Squidex.Controllers.Api.Plans
/// <param name="app">The name of the app.</param>
/// <param name="request">Plan object that needs to be changed.</param>
/// <returns>
/// 201 => Redirected to checkout page.
/// 204 => Plan changed.
/// 400 => Plan not owned by user.
/// 404 => App not found.
@ -88,13 +91,20 @@ namespace Squidex.Controllers.Api.Plans
[MustBeAppOwner]
[HttpPut]
[Route("apps/{app}/plan/")]
[ProducesResponseType(typeof(PlanChangedDto), 200)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiCosts(0.5)]
public async Task<IActionResult> ChangePlanAsync(string app, [FromBody] ChangePlanDto request)
{
await CommandBus.PublishAsync(SimpleMapper.Map(request, new ChangePlan()));
var redirectUri = (string)null;
var context = await CommandBus.PublishAsync(SimpleMapper.Map(request, new ChangePlan()));
if (context.Result<object>() is RedirectToCheckoutResult result)
{
redirectUri = result.Url.ToString();
}
return NoContent();
return Ok(new PlanChangedDto { RedirectUri = redirectUri });
}
}
}

18
src/Squidex/Controllers/Api/Plans/Models/PlanChangedDto.cs

@ -0,0 +1,18 @@
// ==========================================================================
// PlanChangedDto.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Controllers.Api.Plans.Models
{
public class PlanChangedDto
{
/// <summary>
/// Optional redirect uri.
/// </summary>
public string RedirectUri { get; set; }
}
}

18
src/Squidex/app/features/settings/pages/plans/plans-page.component.ts

@ -67,13 +67,17 @@ export class PlansPageComponent extends AppComponentBase implements OnInit {
this.appNameOnce()
.switchMap(app => this.plansService.putPlan(app, new ChangePlanDto(planId), this.version))
.subscribe(dto => {
this.plans =
new AppPlansDto(planId,
this.plans.planOwner,
this.plans.hasPortal,
this.plans.hasConfigured,
this.plans.plans);
this.isDisabled = false;
if (dto.redirectUri && dto.redirectUri.length > 0) {
window.location.replace(dto.redirectUri);
} else {
this.plans =
new AppPlansDto(planId,
this.plans.planOwner,
this.plans.hasPortal,
this.plans.hasConfigured,
this.plans.plans);
this.isDisabled = false;
}
}, error => {
this.notifyError(error);
this.isDisabled = false;

11
src/Squidex/app/shared/services/plans.service.spec.ts

@ -12,6 +12,7 @@ import {
AppPlansDto,
ApiUrlConfig,
ChangePlanDto,
PlanChangedDto,
PlanDto,
PlansService,
Version
@ -93,11 +94,19 @@ describe('PlansService', () => {
const dto = new ChangePlanDto('enterprise');
plansService.putPlan('my-app', dto, version).subscribe();
let planChanged: PlanChangedDto | null = null;
plansService.putPlan('my-app', dto, version).subscribe(result => {
planChanged = result;
});
const req = httpMock.expectOne('http://service/p/api/apps/my-app/plan');
req.flush({ redirectUri: 'my-url' });
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBe(version.value);
expect(planChanged).toBe(new PlanChangedDto('my-url'));
}));
});

12
src/Squidex/app/shared/services/plans.service.ts

@ -40,6 +40,13 @@ export class PlanDto {
}
}
export class PlanChangedDto {
constructor(
public readonly redirectUri: string
) {
}
}
export class ChangePlanDto {
constructor(
public readonly planId: string
@ -80,10 +87,13 @@ export class PlansService {
.pretifyError('Failed to load plans. Please reload.');
}
public putPlan(appName: string, dto: ChangePlanDto, version?: Version): Observable<any> {
public putPlan(appName: string, dto: ChangePlanDto, version?: Version): Observable<PlanChangedDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/plan`);
return HTTP.putVersioned(this.http, url, dto, version)
.map(response => {
return new PlanChangedDto(response.redirectUri);
})
.pretifyError('Failed to change plan. Please reload.');
}
}

40
tests/Squidex.Domain.Apps.Write.Tests/Apps/AppCommandHandlerTests.cs

@ -232,6 +232,41 @@ namespace Squidex.Domain.Apps.Write.Apps
appPlansBillingManager.Verify(x => x.ChangePlanAsync(User.Identifier, app.Id, app.Name, "my-plan"), Times.Once());
}
[Fact]
public async Task ChangePlan_should_not_make_update_for_redirect_result()
{
appPlansProvider.Setup(x => x.IsConfiguredPlan("my-plan")).Returns(true);
appPlansBillingManager.Setup(x => x.ChangePlanAsync(User.Identifier, app.Id, app.Name, "my-plan")).Returns(CreateRedirectResult());
CreateApp();
var context = CreateContextForCommand(new ChangePlan { PlanId = "my-plan" });
await TestUpdate(app, async _ =>
{
await sut.HandleAsync(context);
});
Assert.Null(app.PlanId);
}
[Fact]
public async Task ChangePlan_should_not_call_billing_manager_for_callback()
{
appPlansProvider.Setup(x => x.IsConfiguredPlan("my-plan")).Returns(true);
CreateApp();
var context = CreateContextForCommand(new ChangePlan { PlanId = "my-plan", FromCallback = true });
await TestUpdate(app, async _ =>
{
await sut.HandleAsync(context);
});
appPlansBillingManager.Verify(x => x.ChangePlanAsync(User.Identifier, app.Id, app.Name, "my-plan"), Times.Never());
}
[Fact]
public async Task AddLanguage_should_update_domain_object()
{
@ -279,5 +314,10 @@ namespace Squidex.Domain.Apps.Write.Apps
return app;
}
private static Task<IChangePlanResult> CreateRedirectResult()
{
return Task.FromResult<IChangePlanResult>(new RedirectToCheckoutResult(new Uri("http://squidex.io")));
}
}
}

13
tests/Squidex.Infrastructure.Tests/CQRS/Events/EventReceiverTests.cs

@ -43,7 +43,7 @@ namespace Squidex.Infrastructure.CQRS.Events
this.storedEvents = storedEvents;
}
public Task SubscribeAsync(Func<StoredEvent, Task> onNext, Func<Exception, Task> onError)
public async Task SubscribeAsync(Func<StoredEvent, Task> onNext, Func<Exception, Task> onError)
{
foreach (var storedEvent in storedEvents)
{
@ -52,10 +52,15 @@ namespace Squidex.Infrastructure.CQRS.Events
break;
}
onNext(storedEvent).Wait();
try
{
await onNext(storedEvent);
}
catch (Exception ex)
{
await onError(ex);
}
}
return TaskHelper.Done;
}
public void Dispose()

20
tests/Squidex.Infrastructure.Tests/Timers/CompletionTimerTests.cs

@ -6,9 +6,12 @@
// All rights reserved.
// ==========================================================================
using System.Threading;
using Squidex.Infrastructure.Tasks;
using Xunit;
// ReSharper disable AccessToModifiedClosure
namespace Squidex.Infrastructure.Timers
{
public class CompletionTimerTests
@ -30,5 +33,22 @@ namespace Squidex.Infrastructure.Timers
Assert.True(called);
}
public void Should_invoke_dispose_within_timer()
{
CompletionTimer timer = null;
timer = new CompletionTimer(10, ct =>
{
timer?.Dispose();
return TaskHelper.Done;
}, 10);
Thread.Sleep(1000);
timer.Wakeup();
timer.Dispose();
}
}
}

Loading…
Cancel
Save