Browse Source

Merge pull request #90 from volosoft/berkan/limitations-for-free-account

Add limitations for free account
pull/101/head
Gizem Mutu Kurt 5 years ago
committed by GitHub
parent
commit
5d382e0a81
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 19
      src/EventHub.Application.Contracts/Organizations/FeatureOfPlanDefinitionDto.cs
  2. 3
      src/EventHub.Application.Contracts/Organizations/IOrganizationAppService.cs
  3. 2
      src/EventHub.Application.Contracts/Organizations/OrganizationInListDto.cs
  4. 6
      src/EventHub.Application.Contracts/Organizations/OrganizationProfileDto.cs
  5. 25
      src/EventHub.Application.Contracts/Organizations/PlanInfoDefinitionDto.cs
  6. 4
      src/EventHub.Application/EventHubApplicationAutoMapperProfile.cs
  7. 5
      src/EventHub.Application/Events/EventAppService.cs
  8. 17
      src/EventHub.Application/Organizations/OrganizationAppService.cs
  9. 2
      src/EventHub.BackgroundServices/EventHubBackgroundServicesModule.cs
  10. 42
      src/EventHub.BackgroundServices/Organizations/OrganizationPaidEnrollmentEndDateWorker.cs
  11. 20
      src/EventHub.BackgroundServices/Organizations/OrganizationPlanEndDateNotifier.cs
  12. 5
      src/EventHub.DbMigrator/Data/EventHubSampleDataSeedContributor.cs
  13. 16
      src/EventHub.DbMigrator/EventHubDbMigratorModule.cs
  14. 4
      src/EventHub.Domain.Shared/EventHubErrorCodes.cs
  15. 5
      src/EventHub.Domain.Shared/Localization/EventHub/en.json
  16. 2
      src/EventHub.Domain.Shared/Organizations/OrganizationContst.cs
  17. 6
      src/EventHub.Domain.Shared/Organizations/OrganizationPaymentRequestExtraParameterConfiguration.cs
  18. 3
      src/EventHub.Domain.Shared/Organizations/OrganizationPlanType.cs
  19. 6
      src/EventHub.Domain/Emailing/EmailTemplateDefinitionProvider.cs
  20. 4
      src/EventHub.Domain/Emailing/EmailTemplates.cs
  21. 0
      src/EventHub.Domain/Emailing/Templates/PaidEnrollmentEndDateReminder.tpl
  22. 2
      src/EventHub.Domain/Events/Event.cs
  23. 25
      src/EventHub.Domain/Events/EventManager.cs
  24. 13
      src/EventHub.Domain/Events/Registrations/EventRegistrationManager.cs
  25. 37
      src/EventHub.Domain/Organizations/Organization.cs
  26. 8
      src/EventHub.Domain/Organizations/PaymentRequests/PaymentRequestEventHandler.cs
  27. 28
      src/EventHub.Domain/Organizations/Plans/FeatureOfPlanDefinition.cs
  28. 61
      src/EventHub.Domain/Organizations/Plans/PlanFeatureManager.cs
  29. 72
      src/EventHub.Domain/Organizations/Plans/PlanInfoDefinition.cs
  30. 41
      src/EventHub.Domain/Organizations/Plans/PlanInfoOptions.cs
  31. 2831
      src/EventHub.EntityFrameworkCore/Migrations/20220204083359_Updated_Organization.Designer.cs
  32. 57
      src/EventHub.EntityFrameworkCore/Migrations/20220204083359_Updated_Organization.cs
  33. 10
      src/EventHub.EntityFrameworkCore/Migrations/EventHubDbContextModelSnapshot.cs
  34. 18
      src/EventHub.HttpApi.Host/EventHubHttpApiHostModule.cs
  35. 31
      src/EventHub.HttpApi.Host/appsettings.json
  36. 8
      src/EventHub.HttpApi/Controllers/Organizations/OrganizationController.cs
  37. 18
      src/EventHub.Web/EventHubWebModule.cs
  38. 7
      src/EventHub.Web/Pages/Organizations/Components/OrganizationsArea/_organizationListSection.cshtml
  39. 93
      src/EventHub.Web/Pages/Organizations/Components/_upgradeOrExtendOrganizationSection.cshtml
  40. 74
      src/EventHub.Web/Pages/Organizations/PlanInfoHelper.cs
  41. 5
      src/EventHub.Web/Pages/Organizations/Profile.cshtml
  42. 2
      src/EventHub.Web/Pages/Organizations/Profile.js
  43. 14
      src/EventHub.Web/Pages/Payment/PreCheckout.cshtml
  44. 168
      src/EventHub.Web/Pages/Pricing.cshtml
  45. 41
      src/EventHub.Web/Pages/Pricing.cshtml.cs
  46. 6
      src/EventHub.Web/Pages/Pricing.css
  47. 85
      src/EventHub.Web/PaymentRequests/PremiumPlanInfoOptions.cs
  48. 9
      src/EventHub.Web/appsettings.json
  49. 20
      test/EventHub.Domain.Tests/Events/Registrations/EventRegistrationManager_UnitTests.cs
  50. 14
      test/EventHub.TestBase/EventHubTestBaseModule.cs

19
src/EventHub.Application.Contracts/Organizations/FeatureOfPlanDefinitionDto.cs

@ -0,0 +1,19 @@
using System.Collections.Generic;
namespace EventHub.Organizations;
public class FeatureOfPlanDefinitionDto
{
public uint? MaxAllowedEventsCountInOneYear { get; set; }
public uint? MaxAllowedTracksCountInOneEvent { get; set; }
public uint? MaxAllowedAttendeesCountInOneEvent { get; set; }
public List<string> AdditionalFeatureInfos { get; set; }
public FeatureOfPlanDefinitionDto()
{
AdditionalFeatureInfos = new List<string>();
}
}

3
src/EventHub.Application.Contracts/Organizations/IOrganizationAppService.cs

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services; using Volo.Abp.Application.Services;
@ -21,5 +22,7 @@ namespace EventHub.Organizations
Task UpdateAsync(Guid id, UpdateOrganizationDto input); Task UpdateAsync(Guid id, UpdateOrganizationDto input);
Task<IRemoteStreamContent> GetProfilePictureAsync(Guid id); Task<IRemoteStreamContent> GetProfilePictureAsync(Guid id);
Task<List<PlanInfoDefinitionDto>> GetPlanInfosAsync();
} }
} }

2
src/EventHub.Application.Contracts/Organizations/OrganizationInListDto.cs

@ -11,6 +11,6 @@ namespace EventHub.Organizations
public string Description { get; set; } public string Description { get; set; }
public bool IsPremium { get; set; } public OrganizationPlanType PlanType { get; set; }
} }
} }

6
src/EventHub.Application.Contracts/Organizations/OrganizationProfileDto.cs

@ -27,8 +27,8 @@ namespace EventHub.Organizations
public string MediumUsername { get; set; } public string MediumUsername { get; set; }
public bool IsPremium { get; set; } public OrganizationPlanType PlanType { get; set; }
public DateTime? PremiumEndDate { get; set; } public DateTime? PaidEnrollmentEndDate { get; set; }
} }
} }

25
src/EventHub.Application.Contracts/Organizations/PlanInfoDefinitionDto.cs

@ -0,0 +1,25 @@
namespace EventHub.Organizations;
public class PlanInfoDefinitionDto
{
public OrganizationPlanType PlanType { get; set; }
public string Description { get; set; }
public bool IsActive { get; set; }
public decimal Price { get; set; }
public bool IsExtendable { get; set; }
public int? CanBeExtendedAfterHowManyMonths { get; set; }
public int? OnePaidEnrollmentPeriodAsMonth { get; set; }
public FeatureOfPlanDefinitionDto Feature { get; set; }
public PlanInfoDefinitionDto()
{
Feature = new FeatureOfPlanDefinitionDto();
}
}

4
src/EventHub.Application/EventHubApplicationAutoMapperProfile.cs

@ -5,6 +5,7 @@ using EventHub.Events.Registrations;
using EventHub.Members; using EventHub.Members;
using EventHub.Organizations; using EventHub.Organizations;
using EventHub.Organizations.Memberships; using EventHub.Organizations.Memberships;
using EventHub.Organizations.Plans;
using EventHub.Users; using EventHub.Users;
using Volo.Abp.AutoMapper; using Volo.Abp.AutoMapper;
using Volo.Abp.Identity; using Volo.Abp.Identity;
@ -48,6 +49,9 @@ namespace EventHub
CreateMap<IdentityUser, UserDto>(); CreateMap<IdentityUser, UserDto>();
CreateMap<UserWithoutDetails, UserInListDto>(); CreateMap<UserWithoutDetails, UserInListDto>();
CreateMap<PlanInfoDefinition, PlanInfoDefinitionDto>();
CreateMap<FeatureOfPlanDefinition, FeatureOfPlanDefinitionDto>();
} }
} }
} }

5
src/EventHub.Application/Events/EventAppService.cs

@ -299,10 +299,7 @@ namespace EventHub.Events
var @event = await _eventRepository.GetAsync(id, true); var @event = await _eventRepository.GetAsync(id, true);
await CheckIfValidOwnerAsync(@event); await CheckIfValidOwnerAsync(@event);
@event.AddTrack( await _eventManager.AddTrackAsync(@event, input.Name);
GuidGenerator.Create(),
input.Name
);
await _eventRepository.UpdateAsync(@event); await _eventRepository.UpdateAsync(@event);
} }

17
src/EventHub.Application/Organizations/OrganizationAppService.cs

@ -3,8 +3,11 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using EventHub.Organizations.Memberships; using EventHub.Organizations.Memberships;
using EventHub.Organizations.PaymentRequests;
using EventHub.Organizations.Plans;
using EventHub.Users; using EventHub.Users;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;
using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Dtos;
using Volo.Abp.Authorization; using Volo.Abp.Authorization;
using Volo.Abp.BlobStoring; using Volo.Abp.BlobStoring;
@ -21,19 +24,22 @@ namespace EventHub.Organizations
private readonly OrganizationManager _organizationManager; private readonly OrganizationManager _organizationManager;
private readonly IBlobContainer<OrganizationProfilePictureContainer> _organizationBlobContainer; private readonly IBlobContainer<OrganizationProfilePictureContainer> _organizationBlobContainer;
private readonly IUserRepository _userRepository; private readonly IUserRepository _userRepository;
private readonly IOptions<PlanInfoOptions> _planInfoOptions;
public OrganizationAppService( public OrganizationAppService(
IRepository<Organization, Guid> organizationRepository, IRepository<Organization, Guid> organizationRepository,
IOrganizationMembershipRepository organizationMembershipsRepository, IOrganizationMembershipRepository organizationMembershipsRepository,
OrganizationManager organizationManager, OrganizationManager organizationManager,
IBlobContainer<OrganizationProfilePictureContainer> organizationBlobContainer, IBlobContainer<OrganizationProfilePictureContainer> organizationBlobContainer,
IUserRepository userRepository) IUserRepository userRepository,
IOptions<PlanInfoOptions> planInfoOptions)
{ {
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_organizationMembershipsRepository = organizationMembershipsRepository; _organizationMembershipsRepository = organizationMembershipsRepository;
_organizationManager = organizationManager; _organizationManager = organizationManager;
_organizationBlobContainer = organizationBlobContainer; _organizationBlobContainer = organizationBlobContainer;
_userRepository = userRepository; _userRepository = userRepository;
_planInfoOptions = planInfoOptions;
} }
[Authorize] [Authorize]
@ -163,6 +169,13 @@ namespace EventHub.Organizations
return new RemoteStreamContent(pictureContent, blobName); return new RemoteStreamContent(pictureContent, blobName);
} }
[Authorize]
public async Task<List<PlanInfoDefinitionDto>> GetPlanInfosAsync()
{
var planInfoDefinitions = _planInfoOptions.Value.GetPlanInfos();
return await Task.FromResult(ObjectMapper.Map<List<PlanInfoDefinition>, List<PlanInfoDefinitionDto>>(planInfoDefinitions));
}
private async Task SaveProfilePictureAsync(Guid id, IRemoteStreamContent streamContent) private async Task SaveProfilePictureAsync(Guid id, IRemoteStreamContent streamContent)
{ {
var blobName = id.ToString(); var blobName = id.ToString();
@ -170,4 +183,4 @@ namespace EventHub.Organizations
await _organizationBlobContainer.SaveAsync(blobName, streamContent.GetStream(), overrideExisting: true); await _organizationBlobContainer.SaveAsync(blobName, streamContent.GetStream(), overrideExisting: true);
} }
} }
} }

2
src/EventHub.BackgroundServices/EventHubBackgroundServicesModule.cs

@ -47,7 +47,7 @@ namespace EventHub
context.AddBackgroundWorker<NewEventWorker>(); context.AddBackgroundWorker<NewEventWorker>();
context.AddBackgroundWorker<EventTimingChangeWorker>(); context.AddBackgroundWorker<EventTimingChangeWorker>();
context.AddBackgroundWorker<OrganizationPremiumEndDateWorker>(); context.AddBackgroundWorker<OrganizationPaidEnrollmentEndDateWorker>();
} }
} }
} }

42
src/EventHub.BackgroundServices/Organizations/OrganizationPremiumEndDateWorker.cs → src/EventHub.BackgroundServices/Organizations/OrganizationPaidEnrollmentEndDateWorker.cs

@ -12,9 +12,9 @@ using Volo.Abp.Uow;
namespace EventHub.Organizations namespace EventHub.Organizations
{ {
public class OrganizationPremiumEndDateWorker : AsyncPeriodicBackgroundWorkerBase public class OrganizationPaidEnrollmentEndDateWorker : AsyncPeriodicBackgroundWorkerBase
{ {
public OrganizationPremiumEndDateWorker(AbpAsyncTimer timer, IServiceScopeFactory serviceScopeFactory) : base(timer, serviceScopeFactory) public OrganizationPaidEnrollmentEndDateWorker(AbpAsyncTimer timer, IServiceScopeFactory serviceScopeFactory) : base(timer, serviceScopeFactory)
{ {
Timer.Period = (int)TimeSpan.FromDays(1).TotalMilliseconds; Timer.Period = (int)TimeSpan.FromDays(1).TotalMilliseconds;
} }
@ -22,26 +22,27 @@ namespace EventHub.Organizations
[UnitOfWork(isTransactional: false)] [UnitOfWork(isTransactional: false)]
protected override async Task DoWorkAsync(PeriodicBackgroundWorkerContext workerContext) protected override async Task DoWorkAsync(PeriodicBackgroundWorkerContext workerContext)
{ {
var organizationPremiumEndDateNotifier = workerContext.ServiceProvider.GetRequiredService<OrganizationPremiumEndDateNotifier>(); var organizationPaidEnrollmentEndDateNotifier = workerContext.ServiceProvider.GetRequiredService<OrganizationPlanEndDateNotifier>();
var organizationRepository = workerContext.ServiceProvider.GetRequiredService<IRepository<Organization, Guid>>(); var organizationRepository = workerContext.ServiceProvider.GetRequiredService<IRepository<Organization, Guid>>();
var asyncExecuter = workerContext.ServiceProvider.GetRequiredService<IAsyncQueryableExecuter>(); var asyncExecuter = workerContext.ServiceProvider.GetRequiredService<IAsyncQueryableExecuter>();
var clock = workerContext.ServiceProvider.GetRequiredService<IClock>(); var clock = workerContext.ServiceProvider.GetRequiredService<IClock>();
await SendPremiumReminderEmail(organizationRepository, organizationPremiumEndDateNotifier, asyncExecuter, clock); await SendPremiumReminderEmail(organizationRepository, organizationPaidEnrollmentEndDateNotifier, asyncExecuter, clock);
await UpdatePremiumExpiredOrganization(organizationRepository, asyncExecuter, clock); await UpdatePremiumExpiredOrganization(organizationRepository, asyncExecuter, clock);
} }
private async Task SendPremiumReminderEmail( private async Task SendPremiumReminderEmail(
IRepository<Organization, Guid> organizationRepository, IRepository<Organization, Guid> organizationRepository,
OrganizationPremiumEndDateNotifier organizationPremiumEndDateNotifier, OrganizationPlanEndDateNotifier organizationPlanEndDateNotifier,
IAsyncQueryableExecuter asyncExecuter, IAsyncQueryableExecuter asyncExecuter,
IClock clock) IClock clock)
{ {
var oneMonthLater = clock.Now.AddMonths(1); var oneMonthLater = clock.Now.AddMonths(1);
var queryable = await organizationRepository.GetQueryableAsync(); var queryable = await organizationRepository.GetQueryableAsync();
var query = queryable.Where(x => var query = queryable.Where(x =>
x.IsSendPremiumReminderEmail == false && x.IsSendPaidEnrollmentReminderEmail == false &&
x.PremiumEndDate <= oneMonthLater x.PaidEnrollmentEndDate <= oneMonthLater &&
x.PlanType != OrganizationPlanType.Free
); );
var organizations = await asyncExecuter.ToListAsync(query); var organizations = await asyncExecuter.ToListAsync(query);
@ -50,16 +51,13 @@ namespace EventHub.Organizations
{ {
try try
{ {
if (organization.IsPremium) if (organization.PaidEnrollmentEndDate >= clock.Now)
{ {
if (organization.PremiumEndDate >= clock.Now) await organizationPlanEndDateNotifier.NotifyAsync(organization);
{
await organizationPremiumEndDateNotifier.NotifyAsync(organization);
}
organization.IsSendPremiumReminderEmail = true;
await organizationRepository.UpdateAsync(organization);
} }
organization.IsSendPaidEnrollmentReminderEmail = true;
await organizationRepository.UpdateAsync(organization);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -72,7 +70,8 @@ namespace EventHub.Organizations
{ {
var queryable = await organizationRepository.GetQueryableAsync(); var queryable = await organizationRepository.GetQueryableAsync();
var query = queryable.Where(x => var query = queryable.Where(x =>
x.PremiumEndDate >= clock.Now x.PaidEnrollmentEndDate > clock.Now.Date &&
x.PlanType != OrganizationPlanType.Free
); );
var organizations = await asyncExecuter.ToListAsync(query); var organizations = await asyncExecuter.ToListAsync(query);
@ -81,13 +80,8 @@ namespace EventHub.Organizations
{ {
try try
{ {
if (!organization.IsPremium) organization.SetFreeToPlanType();
{ organization.IsSendPaidEnrollmentReminderEmail = false;
continue;
}
organization.DowngradeToPremium();
organization.IsSendPremiumReminderEmail = false;
await organizationRepository.UpdateAsync(organization); await organizationRepository.UpdateAsync(organization);
} }
catch (Exception ex) catch (Exception ex)
@ -97,4 +91,4 @@ namespace EventHub.Organizations
} }
} }
} }
} }

20
src/EventHub.BackgroundServices/Organizations/OrganizationPremiumEndDateNotifier.cs → src/EventHub.BackgroundServices/Organizations/OrganizationPlanEndDateNotifier.cs

@ -10,18 +10,18 @@ using Volo.Abp.TextTemplating;
namespace EventHub.Organizations namespace EventHub.Organizations
{ {
public class OrganizationPremiumEndDateNotifier : ITransientDependency public class OrganizationPlanEndDateNotifier : ITransientDependency
{ {
private readonly IEmailSender _emailSender; private readonly IEmailSender _emailSender;
private readonly ITemplateRenderer _templateRenderer; private readonly ITemplateRenderer _templateRenderer;
private readonly IRepository<IdentityUser, Guid> _userRepository; private readonly IRepository<IdentityUser, Guid> _userRepository;
private readonly ILogger<OrganizationPremiumEndDateNotifier> _logger; private readonly ILogger<OrganizationPlanEndDateNotifier> _logger;
public OrganizationPremiumEndDateNotifier( public OrganizationPlanEndDateNotifier(
IEmailSender emailSender, IEmailSender emailSender,
ITemplateRenderer templateRenderer, ITemplateRenderer templateRenderer,
IRepository<IdentityUser, Guid> userRepository, IRepository<IdentityUser, Guid> userRepository,
ILogger<OrganizationPremiumEndDateNotifier> logger) ILogger<OrganizationPlanEndDateNotifier> logger)
{ {
_emailSender = emailSender; _emailSender = emailSender;
_templateRenderer = templateRenderer; _templateRenderer = templateRenderer;
@ -31,7 +31,7 @@ namespace EventHub.Organizations
public async Task NotifyAsync(Organization organization) public async Task NotifyAsync(Organization organization)
{ {
if (organization is null || organization.IsSendPremiumReminderEmail || !organization.IsPremium) if (organization is null || organization.IsSendPaidEnrollmentReminderEmail || organization.PlanType != OrganizationPlanType.Free)
{ {
return; return;
} }
@ -39,21 +39,21 @@ namespace EventHub.Organizations
var user = await _userRepository.FindAsync(organization.OwnerUserId); var user = await _userRepository.FindAsync(organization.OwnerUserId);
if (user is null) if (user is null)
{ {
_logger.LogError("(OrganizationPremiumEndDateNotifier) User not found!"); _logger.LogError("(OrganizationPaidEnrollmentEndDateNotifier) User not found!");
return; return;
} }
var templateModel = new var templateModel = new
{ {
OrganizationName = organization.Name, OrganizationName = organization.Name,
EndDate = organization.PremiumEndDate!.Value.ToShortDateString() EndDate = organization.PaidEnrollmentEndDate!.Value.ToShortDateString()
}; };
await _emailSender.SendAsync( await _emailSender.SendAsync(
user.Email, user.Email,
"EventHub: Your Premium Organization is expiring!", "EventHub: Your" + organization.PlanType + "Organization is expiring!",
await _templateRenderer.RenderAsync(EmailTemplates.PremiumEndDateReminder, templateModel) await _templateRenderer.RenderAsync(EmailTemplates.PaidEnrollmentEndDateReminder, templateModel)
); );
} }
} }
} }

5
src/EventHub.DbMigrator/Data/EventHubSampleDataSeedContributor.cs

@ -73,8 +73,7 @@ namespace EventHub.DbMigrator.Data
new IdentityUser(_userSandraId = _guidGenerator.Create(), "SandraWolf", "sandra_wolf@gmail.com"), new IdentityUser(_userSandraId = _guidGenerator.Create(), "SandraWolf", "sandra_wolf@gmail.com"),
new IdentityUser(_userSergeyId = _guidGenerator.Create(), "SergeyPolgul", "sergey_polgul@gmail.com"), new IdentityUser(_userSergeyId = _guidGenerator.Create(), "SergeyPolgul", "sergey_polgul@gmail.com"),
new IdentityUser(_userWellyId = _guidGenerator.Create(), "WellyTambunan", "welly_tambunan@gmail.com"), new IdentityUser(_userWellyId = _guidGenerator.Create(), "WellyTambunan", "welly_tambunan@gmail.com"),
new IdentityUser(_userAlessandroId = _guidGenerator.Create(), "AlessandroMuci", new IdentityUser(_userAlessandroId = _guidGenerator.Create(), "AlessandroMuci", "alessandro_muci@gmail.com"),
"alessandro_muci@gmail.com"),
new IdentityUser(_userMarkId = _guidGenerator.Create(), "MarkGodfrey", "mark_godfrey@gmail.com"), new IdentityUser(_userMarkId = _guidGenerator.Create(), "MarkGodfrey", "mark_godfrey@gmail.com"),
new IdentityUser(_userTonyId = _guidGenerator.Create(), "TonyBurton", "tony_burton@gmail.com"), new IdentityUser(_userTonyId = _guidGenerator.Create(), "TonyBurton", "tony_burton@gmail.com"),
new IdentityUser(_guidGenerator.Create(), "DavidGraus", "david_graus@gmail.com"), new IdentityUser(_guidGenerator.Create(), "DavidGraus", "david_graus@gmail.com"),
@ -385,4 +384,4 @@ namespace EventHub.DbMigrator.Data
} }
} }
} }
#endif #endif

16
src/EventHub.DbMigrator/EventHubDbMigratorModule.cs

@ -1,5 +1,7 @@
using EventHub.Admin; using EventHub.Admin;
using EventHub.EntityFrameworkCore; using EventHub.EntityFrameworkCore;
using EventHub.Organizations;
using EventHub.Organizations.Plans;
using Volo.Abp.Autofac; using Volo.Abp.Autofac;
using Volo.Abp.BackgroundJobs; using Volo.Abp.BackgroundJobs;
using Volo.Abp.Modularity; using Volo.Abp.Modularity;
@ -17,6 +19,20 @@ namespace EventHub.DbMigrator
public override void ConfigureServices(ServiceConfigurationContext context) public override void ConfigureServices(ServiceConfigurationContext context)
{ {
Configure<AbpBackgroundJobOptions>(options => options.IsJobExecutionEnabled = false); Configure<AbpBackgroundJobOptions>(options => options.IsJobExecutionEnabled = false);
#if DEBUG
Configure<PlanInfoOptions>(options =>
{
options.Infos.Add(new PlanInfoDefinition
{
PlanType = OrganizationPlanType.Free,
Description = "This is a default plan.",
IsActive = true,
Price = 0,
IsExtendable = false
});
});
} }
#endif
} }
} }

4
src/EventHub.Domain.Shared/EventHubErrorCodes.cs

@ -20,6 +20,8 @@
public const string TrackNotFound = "EventHub:TrackNotFound"; public const string TrackNotFound = "EventHub:TrackNotFound";
public const string SessionNotFound = "EventHub:SessionNotFound"; public const string SessionNotFound = "EventHub:SessionNotFound";
public const string SessionTitleAlreadyExist = "EventHub:SessionTitleAlreadyExist"; public const string SessionTitleAlreadyExist = "EventHub:SessionTitleAlreadyExist";
public const string CannotCreateNewEvent = "EventHub:CannotCreateNewEvent";
public const string CannotAddNewTrack = "EventHub:CannotAddNewTrack";
public const string CannotRegisterToEvent = "EventHub:CannotRegisterToEvent";
} }
} }

5
src/EventHub.Domain.Shared/Localization/EventHub/en.json

@ -159,6 +159,9 @@
"EventHub:SessionNotFound": "The session not found!", "EventHub:SessionNotFound": "The session not found!",
"EventHub:SessionTimeShouldBeInTheEventTime": "Session time should be in the event time!", "EventHub:SessionTimeShouldBeInTheEventTime": "Session time should be in the event time!",
"EventHub:SessionTimeConflictsWithAnExistingSession": "Session time conflicts with an existing session!", "EventHub:SessionTimeConflictsWithAnExistingSession": "Session time conflicts with an existing session!",
"EventHub:SessionTitleAlreadyExist": "The session {Title} already exists" "EventHub:SessionTitleAlreadyExist": "The session {Title} already exists",
"EventHub:CannotCreateNewEvent": "Cannot create a new event, you need to upgrade your organization!",
"EventHub:CannotAddNewTrack": "Cannot add a new track, you need to upgrade your organization!",
"EventHub:CannotRegisterToEvent": "The maximum number of participants allowed by the organization has been reached!"
} }
} }

2
src/EventHub.Domain.Shared/Organizations/OrganizationContst.cs

@ -27,6 +27,8 @@
public const int MaxMediumUsernameLength = 24; public const int MaxMediumUsernameLength = 24;
public const string ProductNamePrefix = "EventHub";
public static string[] AllowedProfilePictureExtensions = { ".jpg", ".png" }; public static string[] AllowedProfilePictureExtensions = { ".jpg", ".png" };
} }
} }

6
src/EventHub.Domain.Shared/Organizations/OrganizationPaymentRequestExtraParameterConfiguration.cs

@ -6,6 +6,8 @@ namespace EventHub.Organizations
public bool IsExtend { get; set; } public bool IsExtend { get; set; }
public int PremiumPeriodAsMonth { get; set; } public int? PremiumPeriodAsMonth { get; set; }
public OrganizationPlanType TargetPlanType { get; set; }
} }
} }

3
src/EventHub.Domain.Shared/Organizations/OrganizationPlanType.cs

@ -2,6 +2,7 @@ namespace EventHub.Organizations
{ {
public enum OrganizationPlanType : byte public enum OrganizationPlanType : byte
{ {
Free = 0,
Premium, Premium,
} }
} }

6
src/EventHub.Domain/Emailing/EmailTemplateDefinitionProvider.cs

@ -76,12 +76,12 @@ namespace EventHub.Emailing
context.Add( context.Add(
new TemplateDefinition( new TemplateDefinition(
EmailTemplates.PremiumEndDateReminder) EmailTemplates.PaidEnrollmentEndDateReminder)
.WithVirtualFilePath( .WithVirtualFilePath(
"/Emailing/Templates/PremiumEndDateReminder.tpl", "/Emailing/Templates/PaidEnrollmentEndDateReminder.tpl",
isInlineLocalized: true isInlineLocalized: true
) )
); );
} }
} }
} }

4
src/EventHub.Domain/Emailing/EmailTemplates.cs

@ -16,6 +16,6 @@ namespace EventHub.Emailing
public const string PaymentRequestFailed = "EventHub.Organization.PaymentRequestFailed"; public const string PaymentRequestFailed = "EventHub.Organization.PaymentRequestFailed";
public const string PremiumEndDateReminder = "EventHub.Organization.PremiumEndDateReminder"; public const string PaidEnrollmentEndDateReminder = "EventHub.Organization.PaidEnrollmentEndDateReminder";
} }
} }

0
src/EventHub.Domain/Emailing/Templates/PremiumEndDateReminder.tpl → src/EventHub.Domain/Emailing/Templates/PaidEnrollmentEndDateReminder.tpl

2
src/EventHub.Domain/Events/Event.cs

@ -129,7 +129,7 @@ namespace EventHub.Events
return this; return this;
} }
public Event AddTrack(Guid trackId, string name) internal Event AddTrack(Guid trackId, string name)
{ {
if (Tracks.Any(x => x.Name == name)) if (Tracks.Any(x => x.Name == name))
{ {

25
src/EventHub.Domain/Events/EventManager.cs

@ -3,6 +3,7 @@ using System.Threading.Tasks;
using EventHub.Countries; using EventHub.Countries;
using EventHub.Events.Registrations; using EventHub.Events.Registrations;
using EventHub.Organizations; using EventHub.Organizations;
using EventHub.Organizations.Plans;
using Volo.Abp; using Volo.Abp;
using Volo.Abp.Domain.Repositories; using Volo.Abp.Domain.Repositories;
using Volo.Abp.Domain.Services; using Volo.Abp.Domain.Services;
@ -14,15 +15,17 @@ namespace EventHub.Events
private readonly EventUrlCodeGenerator _eventUrlCodeGenerator; private readonly EventUrlCodeGenerator _eventUrlCodeGenerator;
private readonly IRepository<EventRegistration, Guid> _eventRegistrationRepository; private readonly IRepository<EventRegistration, Guid> _eventRegistrationRepository;
private readonly IRepository<Country, Guid> _countriesRepository; private readonly IRepository<Country, Guid> _countriesRepository;
private readonly PlanFeatureManager _planFeatureManager;
public EventManager( public EventManager(
EventUrlCodeGenerator eventUrlCodeGenerator, EventUrlCodeGenerator eventUrlCodeGenerator,
IRepository<EventRegistration, Guid> eventRegistrationRepository, IRepository<EventRegistration, Guid> eventRegistrationRepository,
IRepository<Country, Guid> countriesRepository) IRepository<Country, Guid> countriesRepository,
PlanFeatureManager planFeatureManager)
{ {
_eventUrlCodeGenerator = eventUrlCodeGenerator; _eventUrlCodeGenerator = eventUrlCodeGenerator;
_eventRegistrationRepository = eventRegistrationRepository; _eventRegistrationRepository = eventRegistrationRepository;
_countriesRepository = countriesRepository; _countriesRepository = countriesRepository;
_planFeatureManager = planFeatureManager;
} }
public async Task<Event> CreateAsync( public async Task<Event> CreateAsync(
@ -32,6 +35,11 @@ namespace EventHub.Events
DateTime endTime, DateTime endTime,
string description) string description)
{ {
if (!await _planFeatureManager.CanCreateNewEventAsync(organization.Id))
{
throw new BusinessException(EventHubErrorCodes.CannotCreateNewEvent);
}
return new Event( return new Event(
GuidGenerator.Create(), GuidGenerator.Create(),
organization.Id, organization.Id,
@ -42,6 +50,19 @@ namespace EventHub.Events
description description
); );
} }
public async Task AddTrackAsync(Event @event, string name)
{
if (!await _planFeatureManager.CanAddNewTrackAsync(@event))
{
throw new BusinessException(EventHubErrorCodes.CannotAddNewTrack);
}
@event.AddTrack(
GuidGenerator.Create(),
name
);
}
public async Task SetCapacityAsync( public async Task SetCapacityAsync(
Event @event, Event @event,

13
src/EventHub.Domain/Events/Registrations/EventRegistrationManager.cs

@ -1,4 +1,5 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using EventHub.Organizations.Plans;
using Volo.Abp; using Volo.Abp;
using Volo.Abp.Domain.Repositories; using Volo.Abp.Domain.Repositories;
using Volo.Abp.Domain.Services; using Volo.Abp.Domain.Services;
@ -8,20 +9,23 @@ using Volo.Abp.Timing;
namespace EventHub.Events.Registrations namespace EventHub.Events.Registrations
{ {
public class EventRegistrationManager : IDomainService public class EventRegistrationManager : DomainService
{ {
private readonly IEventRegistrationRepository _eventRegistrationRepository; private readonly IEventRegistrationRepository _eventRegistrationRepository;
private readonly IGuidGenerator _guidGenerator; private readonly IGuidGenerator _guidGenerator;
private readonly IClock _clock; private readonly IClock _clock;
private readonly PlanFeatureManager _planFeatureManager;
public EventRegistrationManager( public EventRegistrationManager(
IEventRegistrationRepository eventRegistrationRepository, IEventRegistrationRepository eventRegistrationRepository,
IGuidGenerator guidGenerator, IGuidGenerator guidGenerator,
IClock clock) IClock clock,
PlanFeatureManager planFeatureManager)
{ {
_eventRegistrationRepository = eventRegistrationRepository; _eventRegistrationRepository = eventRegistrationRepository;
_guidGenerator = guidGenerator; _guidGenerator = guidGenerator;
_clock = clock; _clock = clock;
_planFeatureManager = planFeatureManager;
} }
public async Task RegisterAsync( public async Task RegisterAsync(
@ -47,6 +51,11 @@ namespace EventHub.Events.Registrations
.WithData("EventTitle", @event.Title); .WithData("EventTitle", @event.Title);
} }
} }
if (!await _planFeatureManager.CanRegisterToEventAsync(@event))
{
throw new BusinessException(EventHubErrorCodes.CannotRegisterToEvent);
}
await _eventRegistrationRepository.InsertAsync( await _eventRegistrationRepository.InsertAsync(
new EventRegistration( new EventRegistration(

37
src/EventHub.Domain/Organizations/Organization.cs

@ -1,6 +1,8 @@
using System; using System;
using JetBrains.Annotations;
using Volo.Abp; using Volo.Abp;
using Volo.Abp.Domain.Entities.Auditing; using Volo.Abp.Domain.Entities.Auditing;
using Volo.Abp.Validation;
namespace EventHub.Organizations namespace EventHub.Organizations
{ {
@ -28,11 +30,11 @@ namespace EventHub.Organizations
public int MemberCount { get; internal set; } public int MemberCount { get; internal set; }
public bool IsPremium { get; private set; } public OrganizationPlanType PlanType { get; private set; }
public DateTime? PremiumEndDate { get; private set; } public DateTime? PaidEnrollmentEndDate { get; private set; }
public bool IsSendPremiumReminderEmail { get; set; } public bool IsSendPaidEnrollmentReminderEmail { get; set; }
private Organization() private Organization()
{ {
@ -50,6 +52,7 @@ namespace EventHub.Organizations
SetName(name); SetName(name);
SetDisplayName(displayName); SetDisplayName(displayName);
SetDescription(description); SetDescription(description);
SetFreeToPlanType();
} }
internal Organization SetName(string name) internal Organization SetName(string name)
@ -70,38 +73,40 @@ namespace EventHub.Organizations
return this; return this;
} }
internal Organization UpgradeToPremium(DateTime endDate) internal Organization UpgradeToPlanType(OrganizationPlanType planType, DateTime endDate)
{ {
SetPremiumStatus(true, endDate); SetPlanType(planType, endDate);
return this; return this;
} }
public Organization DowngradeToPremium() public Organization SetFreeToPlanType()
{ {
if (!IsPremium) if (PlanType == OrganizationPlanType.Free)
{ {
return this; return this;
} }
SetPremiumStatus(false); SetPlanType(OrganizationPlanType.Free);
return this; return this;
} }
private Organization SetPremiumStatus(bool isPremium, DateTime? endDate = null) private Organization SetPlanType(OrganizationPlanType planType, DateTime? endDate = null)
{ {
if (isPremium) if (!Enum.IsDefined(typeof(OrganizationPlanType), planType))
{ {
Check.NotNull(endDate, nameof(endDate)); throw new AbpValidationException();
IsPremium = true;
PremiumEndDate = endDate;
return this;
} }
IsPremium = false; if (planType != OrganizationPlanType.Free)
{
Check.NotNull(endDate, nameof(endDate));
}
PlanType = planType;
PaidEnrollmentEndDate = endDate;
return this; return this;
} }
} }

8
src/EventHub.Domain/Organizations/PaymentRequests/PaymentRequestEventHandler.cs

@ -134,15 +134,15 @@ namespace EventHub.Organizations.PaymentRequests
if (organizationPaymentRequestExtraParameter.IsExtend) if (organizationPaymentRequestExtraParameter.IsExtend)
{ {
organization.UpgradeToPremium( organization.UpgradeToPlanType(organizationPaymentRequestExtraParameter.TargetPlanType,
organization.PremiumEndDate!.Value.AddMonths(organizationPaymentRequestExtraParameter.PremiumPeriodAsMonth)); organization.PaidEnrollmentEndDate!.Value.AddMonths(organizationPaymentRequestExtraParameter.PremiumPeriodAsMonth!.Value));
} }
else else
{ {
organization.UpgradeToPremium(DateTime.Now.AddMonths(organizationPaymentRequestExtraParameter.PremiumPeriodAsMonth)); organization.UpgradeToPlanType(organizationPaymentRequestExtraParameter.TargetPlanType, DateTime.Now.AddMonths(organizationPaymentRequestExtraParameter.PremiumPeriodAsMonth!.Value));
} }
return await _organizationRepository.UpdateAsync(organization, true); return await _organizationRepository.UpdateAsync(organization, true);
} }
} }
} }

28
src/EventHub.Domain/Organizations/Plans/FeatureOfPlanDefinition.cs

@ -0,0 +1,28 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using JetBrains.Annotations;
namespace EventHub.Organizations.Plans;
public class FeatureOfPlanDefinition
{
[CanBeNull]
[Range(0, uint.MaxValue)]
public uint? MaxAllowedEventsCountInOneYear { get; set; }
[CanBeNull]
[Range(0, uint.MaxValue)]
public uint? MaxAllowedTracksCountInOneEvent { get; set; }
[CanBeNull]
[Range(0, uint.MaxValue)]
public uint? MaxAllowedAttendeesCountInOneEvent { get; set; }
[NotNull]
public List<string> AdditionalFeatureInfos { get; set; }
public FeatureOfPlanDefinition()
{
AdditionalFeatureInfos = new List<string>();
}
}

61
src/EventHub.Domain/Organizations/Plans/PlanFeatureManager.cs

@ -0,0 +1,61 @@
using System;
using System.Threading.Tasks;
using EventHub.Events;
using EventHub.Events.Registrations;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Timing;
namespace EventHub.Organizations.Plans;
public class PlanFeatureManager : ITransientDependency
{
private readonly IOptions<PlanInfoOptions> _planInfoOptions;
private readonly IRepository<Organization, Guid> _organizationRepository;
private readonly IRepository<Event, Guid> _eventRepository;
private readonly IEventRegistrationRepository _eventRegistrationRepository;
private readonly IClock _clock;
public PlanFeatureManager(
IOptions<PlanInfoOptions> planInfoOptions,
IRepository<Organization, Guid> organizationRepository,
IRepository<Event, Guid> eventRepository,
IEventRegistrationRepository eventRegistrationRepository,
IClock clock)
{
_planInfoOptions = planInfoOptions;
_organizationRepository = organizationRepository;
_eventRepository = eventRepository;
_eventRegistrationRepository = eventRegistrationRepository;
_clock = clock;
}
public virtual async Task<bool> CanCreateNewEventAsync(Guid organizationId)
{
var currentPlanOfOrganization = await GetCurrentPlanByOrganizationIdAsync(organizationId);
var totalEventCountByOrganizationInThisYear = await _eventRepository.CountAsync(x => x.OrganizationId == organizationId && x.CreationTime.Year == _clock.Now.Year);
return currentPlanOfOrganization.Feature.MaxAllowedEventsCountInOneYear > totalEventCountByOrganizationInThisYear;
}
public virtual async Task<bool> CanAddNewTrackAsync(Event @event)
{
var currentPlanOfOrganization = await GetCurrentPlanByOrganizationIdAsync(@event.OrganizationId);
var tracksCount = @event.Tracks.Count;
return currentPlanOfOrganization.Feature.MaxAllowedTracksCountInOneEvent > tracksCount;
}
public virtual async Task<bool> CanRegisterToEventAsync(Event @event)
{
var currentPlanOfOrganization = await GetCurrentPlanByOrganizationIdAsync(@event.OrganizationId);
var attendeesCount = await _eventRegistrationRepository.CountAsync(x => x.EventId == @event.Id);
return currentPlanOfOrganization.Feature.MaxAllowedAttendeesCountInOneEvent > attendeesCount;
}
private async Task<PlanInfoDefinition> GetCurrentPlanByOrganizationIdAsync(Guid organizationId)
{
var organization = await _organizationRepository.GetAsync(organizationId);
return _planInfoOptions.Value.GetPlanInfoByType(organization.PlanType);
}
}

72
src/EventHub.Domain/Organizations/Plans/PlanInfoDefinition.cs

@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using JetBrains.Annotations;
using Volo.Abp;
namespace EventHub.Organizations.Plans;
public class PlanInfoDefinition
{
public const string PlanInfo = "PlanInfos";
public OrganizationPlanType PlanType { get; set; }
public string Description { get; set; }
public bool IsActive { get; set; }
[Range(1.0, 100.0)]
public decimal Price { get; set; } = 1.0M;
public bool IsExtendable { get; set; }
[CanBeNull]
[Range(0, 12)]
public int? CanBeExtendedAfterHowManyMonths { get; set; }
[CanBeNull]
[Range(1, 24)]
public int? OnePaidEnrollmentPeriodAsMonth { get; set; } = 12;
public FeatureOfPlanDefinition Feature { get; set; }
public PlanInfoDefinition()
{
Feature = new FeatureOfPlanDefinition
{
MaxAllowedEventsCountInOneYear = uint.MaxValue,
MaxAllowedTracksCountInOneEvent = uint.MaxValue,
MaxAllowedAttendeesCountInOneEvent = uint.MaxValue,
AdditionalFeatureInfos = new List<string>()
};
}
public static bool IsValid(List<PlanInfoDefinition> definitions)
{
foreach (var definition in definitions)
{
var isValidPlanType = Enum.IsDefined(typeof(OrganizationPlanType), definition.PlanType);
if (!isValidPlanType)
{
return false;
}
var isExistSamePlan = definitions.Count(x => x.Price == definition.Price && x.PlanType == definition.PlanType);
if (isExistSamePlan > 1)
{
return false;
}
if (definition.IsActive && definition.IsExtendable)
{
Check.NotNull(definition.OnePaidEnrollmentPeriodAsMonth, nameof(definition.OnePaidEnrollmentPeriodAsMonth));
Check.NotNull(definition.CanBeExtendedAfterHowManyMonths, nameof(definition.CanBeExtendedAfterHowManyMonths));
return definition.OnePaidEnrollmentPeriodAsMonth > definition.CanBeExtendedAfterHowManyMonths;
}
}
return true;
}
}

41
src/EventHub.Domain/Organizations/Plans/PlanInfoOptions.cs

@ -0,0 +1,41 @@
using System.Collections.Generic;
using System.Linq;
namespace EventHub.Organizations.Plans;
public class PlanInfoOptions
{
public List<PlanInfoDefinition> Infos { get; }
public PlanInfoOptions()
{
Infos = new List<PlanInfoDefinition>();
}
public PlanInfoOptions AddPlanInfos(List<PlanInfoDefinition> infos)
{
foreach (var info in infos)
{
AddPlanInfo(info);
}
return this;
}
private PlanInfoOptions AddPlanInfo(PlanInfoDefinition info)
{
Infos.AddIfNotContains(info);
return this;
}
public List<PlanInfoDefinition> GetPlanInfos()
{
return Infos.Where(x => x.IsActive).ToList();
}
public PlanInfoDefinition GetPlanInfoByType(OrganizationPlanType type)
{
return Infos.SingleOrDefault(x => x.IsActive && x.PlanType == type);
}
}

2831
src/EventHub.EntityFrameworkCore/Migrations/20220204083359_Updated_Organization.Designer.cs

File diff suppressed because it is too large

57
src/EventHub.EntityFrameworkCore/Migrations/20220204083359_Updated_Organization.cs

@ -0,0 +1,57 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace EventHub.Migrations
{
public partial class Updated_Organization : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "IsPremium",
table: "EhOrganizations");
migrationBuilder.RenameColumn(
name: "PremiumEndDate",
table: "EhOrganizations",
newName: "PaidEnrollmentEndDate");
migrationBuilder.RenameColumn(
name: "IsSendPremiumReminderEmail",
table: "EhOrganizations",
newName: "IsSendPaidEnrollmentReminderEmail");
migrationBuilder.AddColumn<byte>(
name: "PlanType",
table: "EhOrganizations",
type: "smallint",
nullable: false,
defaultValue: (byte)0);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "PlanType",
table: "EhOrganizations");
migrationBuilder.RenameColumn(
name: "PaidEnrollmentEndDate",
table: "EhOrganizations",
newName: "PremiumEndDate");
migrationBuilder.RenameColumn(
name: "IsSendPaidEnrollmentReminderEmail",
table: "EhOrganizations",
newName: "IsSendPremiumReminderEmail");
migrationBuilder.AddColumn<bool>(
name: "IsPremium",
table: "EhOrganizations",
type: "boolean",
nullable: false,
defaultValue: false);
}
}
}

10
src/EventHub.EntityFrameworkCore/Migrations/EventHubDbContextModelSnapshot.cs

@ -376,10 +376,7 @@ namespace EventHub.Migrations
.HasDefaultValue(false) .HasDefaultValue(false)
.HasColumnName("IsDeleted"); .HasColumnName("IsDeleted");
b.Property<bool>("IsPremium") b.Property<bool>("IsSendPaidEnrollmentReminderEmail")
.HasColumnType("boolean");
b.Property<bool>("IsSendPremiumReminderEmail")
.HasColumnType("boolean"); .HasColumnType("boolean");
b.Property<DateTime?>("LastModificationTime") b.Property<DateTime?>("LastModificationTime")
@ -404,9 +401,12 @@ namespace EventHub.Migrations
b.Property<Guid>("OwnerUserId") b.Property<Guid>("OwnerUserId")
.HasColumnType("uuid"); .HasColumnType("uuid");
b.Property<DateTime?>("PremiumEndDate") b.Property<DateTime?>("PaidEnrollmentEndDate")
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp with time zone");
b.Property<byte>("PlanType")
.HasColumnType("smallint");
b.Property<string>("TwitterUsername") b.Property<string>("TwitterUsername")
.HasColumnType("text"); .HasColumnType("text");

18
src/EventHub.HttpApi.Host/EventHubHttpApiHostModule.cs

@ -2,9 +2,11 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq;
using EventHub.EntityFrameworkCore; using EventHub.EntityFrameworkCore;
using EventHub.Events; using EventHub.Events;
using EventHub.Organizations; using EventHub.Organizations;
using EventHub.Organizations.Plans;
using EventHub.Utils; using EventHub.Utils;
using EventHub.Web; using EventHub.Web;
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.JwtBearer;
@ -15,6 +17,7 @@ using Microsoft.AspNetCore.Localization;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Models;
using StackExchange.Redis; using StackExchange.Redis;
using Volo.Abp; using Volo.Abp;
@ -63,6 +66,7 @@ namespace EventHub
ConfigureBackgroundJobs(); ConfigureBackgroundJobs();
ConfigureAutoApiControllers(); ConfigureAutoApiControllers();
ConfigureTiming(); ConfigureTiming();
ConfigurePremiumPlanInfo(context, configuration);
} }
private void ConfigureAutoApiControllers() private void ConfigureAutoApiControllers()
@ -190,6 +194,20 @@ namespace EventHub
{ {
Configure<AbpClockOptions>(options => { options.Kind = DateTimeKind.Utc; }); Configure<AbpClockOptions>(options => { options.Kind = DateTimeKind.Utc; });
} }
private void ConfigurePremiumPlanInfo(ServiceConfigurationContext context, IConfiguration configuration)
{
context.Services.AddOptions<List<PlanInfoDefinition>>()
.Bind(configuration.GetSection(PlanInfoDefinition.PlanInfo))
.ValidateDataAnnotations()
.Validate(PlanInfoDefinition.IsValid, "PlanInfoDefinition is not valid!");
var planInfos = context.Services.GetRequiredServiceLazy<IOptions<List<PlanInfoDefinition>>>();
Configure<PlanInfoOptions>(options =>
{
options.AddPlanInfos(planInfos.Value.Value);
});
}
public override void OnApplicationInitialization(ApplicationInitializationContext context) public override void OnApplicationInitialization(ApplicationInitializationContext context)
{ {

31
src/EventHub.HttpApi.Host/appsettings.json

@ -18,5 +18,34 @@
"Secret": "PAYPAL_SECRET", "Secret": "PAYPAL_SECRET",
"Environment": "Sandbox" "Environment": "Sandbox"
} }
} },
"PlanInfos": [
{
"PlanType": "Free",
"Description": "For individuals and small organizations.",
"IsActive": true,
"Price": 0,
"IsExtendable": false,
"Feature": {
"MaxAllowedEventsCountInOneYear": 2,
"MaxAllowedTracksCountInOneEvent": 2,
"MaxAllowedAttendeesCountInOneEvent": 1000
}
},
{
"PlanType": "Premium",
"Description": "For those organize regular events.",
"IsActive": true,
"Price": 29.90,
"IsExtendable": true,
"CanBeExtendedAfterHowManyMonths": 9,
"OnePaidEnrollmentPeriodAsMonth": 12,
"Feature": {
"AdditionalFeatureInfos": [
"Be more prominent in the organization list.",
"Premium badge in the organization page."
]
}
}
]
} }

8
src/EventHub.HttpApi/Controllers/Organizations/OrganizationController.cs

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using EventHub.Organizations; using EventHub.Organizations;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -84,5 +85,12 @@ namespace EventHub.Controllers.Organizations
return remoteStreamContent; return remoteStreamContent;
} }
[HttpGet]
[Route("plan-infos")]
public async Task<List<PlanInfoDefinitionDto>> GetPlanInfosAsync()
{
return await _organizationAppService.GetPlanInfosAsync();
}
} }
} }

18
src/EventHub.Web/EventHubWebModule.cs

@ -2,7 +2,6 @@ using System;
using System.IO; using System.IO;
using EventHub.Localization; using EventHub.Localization;
using EventHub.Web.Menus; using EventHub.Web.Menus;
using EventHub.Web.PaymentRequests;
using EventHub.Web.Theme; using EventHub.Web.Theme;
using EventHub.Web.Theme.Bundling; using EventHub.Web.Theme.Bundling;
using EventHub.Web.Utils; using EventHub.Web.Utils;
@ -88,7 +87,6 @@ namespace EventHub.Web
ConfigureCookies(context); ConfigureCookies(context);
ConfigureSwaggerServices(context.Services); ConfigureSwaggerServices(context.Services);
ConfigureRazorPageOptions(); ConfigureRazorPageOptions();
ConfigurePremiumPlanInfo(context, configuration);
} }
private void ConfigureBundles() private void ConfigureBundles()
@ -235,22 +233,6 @@ namespace EventHub.Web
.PersistKeysToStackExchangeRedis(redis, "EventHub-Protection-Keys"); .PersistKeysToStackExchangeRedis(redis, "EventHub-Protection-Keys");
} }
private void ConfigurePremiumPlanInfo(ServiceConfigurationContext context, IConfiguration configuration)
{
context.Services.AddOptions<PremiumPlanInfoOptions>()
.Bind(configuration.GetSection(PremiumPlanInfoOptions.OrganizationPlanInfo))
.ValidateDataAnnotations()
.Validate(config =>
{
if (config.IsActive)
{
return config.OnePremiumPeriodAsMonth > config.CanBeExtendedAfterHowManyMonths;
}
return true;
}, "OnePremiumPeriodAsMonth must be greater than CanBeExtendedAfterHowManyMonths.");
}
public override void OnApplicationInitialization(ApplicationInitializationContext context) public override void OnApplicationInitialization(ApplicationInitializationContext context)
{ {
var app = context.GetApplicationBuilder(); var app = context.GetApplicationBuilder();

7
src/EventHub.Web/Pages/Organizations/Components/OrganizationsArea/_organizationListSection.cshtml

@ -1,3 +1,4 @@
@using EventHub.Organizations
@using EventHub.Web @using EventHub.Web
@using Microsoft.Extensions.Options @using Microsoft.Extensions.Options
@model List<EventHub.Organizations.OrganizationInListDto> @model List<EventHub.Organizations.OrganizationInListDto>
@ -14,9 +15,9 @@
<div class="event-info"> <div class="event-info">
<h5> <h5>
@organization.DisplayName @organization.DisplayName
@if (organization.IsPremium) @if (organization.PlanType != OrganizationPlanType.Free)
{ {
<span class="align-top text-dark font-weight-bold" style="font-size: x-small" abp-badge-pill="Warning"> Premium </span> <span class="align-top text-dark font-weight-bold" style="font-size: x-small" abp-badge-pill="Warning"> @organization.PlanType </span>
} }
</h5> </h5>
@ -29,4 +30,4 @@
</div> </div>
</div> </div>
</div> </div>
} }

93
src/EventHub.Web/Pages/Organizations/Components/_upgradeOrExtendOrganizationSection.cshtml

@ -1,56 +1,67 @@
@using EventHub.Web.PaymentRequests @using EventHub.Organizations
@using Microsoft.Extensions.Options
@model EventHub.Organizations.OrganizationProfileDto @model EventHub.Organizations.OrganizationProfileDto
@inject IOptionsSnapshot<PremiumPlanInfoOptions> PremiumPlanInfoOptionsSnapshot @inject IOrganizationAppService OrganizationAppService
<abp-container class="mt-4"> @{
<abp-row> PlanInfoDefinitionDto currentPlanInfo = null;
@if (Model.IsPremium && PremiumPlanInfoOptionsSnapshot.Value.IsActive && PremiumPlanInfoOptionsSnapshot.Value.IsExtendable && Model.PremiumEndDate!.Value.IsBetween(DateTime.Now, DateTime.Now.AddMonths(PremiumPlanInfoOptionsSnapshot.Value.OnePremiumPeriodAsMonth - PremiumPlanInfoOptionsSnapshot.Value.CanBeExtendedAfterHowManyMonths))) if (Model is not null)
{ {
<span class="mb-2 ms-5 text-center strong">Premium end date: @Model.PremiumEndDate!.Value.ToShortDateString()</span> var planInfos = await OrganizationAppService.GetPlanInfosAsync();
} currentPlanInfo = planInfos.SingleOrDefault(x => x.PlanType == Model.PlanType);
<abp-column> }
<a href="/organization/edit/@Model.Name" class="btn btn-success text-white">Edit</a> }
</abp-column>
<abp-column size="_9"> @if (Model is not null && currentPlanInfo is not null)
@if (PremiumPlanInfoOptionsSnapshot.Value.IsActive) {
<abp-container class="mt-4">
<abp-row>
@if (Model.PlanType != OrganizationPlanType.Free && currentPlanInfo.IsActive && currentPlanInfo.IsExtendable && Model.PaidEnrollmentEndDate!.Value.IsBetween(DateTime.Now, DateTime.Now.AddMonths(currentPlanInfo.OnePaidEnrollmentPeriodAsMonth!.Value - currentPlanInfo.CanBeExtendedAfterHowManyMonths!.Value)))
{ {
@if (Model.IsPremium) <span class="mb-2 ms-5 text-center strong">@Model.PlanType end date: @Model.PaidEnrollmentEndDate!.Value.ToShortDateString()</span>
}
<abp-column>
<a href="/organization/edit/@Model.Name" class="btn btn-success text-white">Edit</a>
</abp-column>
<abp-column size="_9">
@if (currentPlanInfo.IsActive)
{ {
@if (PremiumPlanInfoOptionsSnapshot.Value.IsExtendable && Model.PremiumEndDate!.Value.IsBetween(DateTime.Now, DateTime.Now.AddMonths(PremiumPlanInfoOptionsSnapshot.Value.OnePremiumPeriodAsMonth - PremiumPlanInfoOptionsSnapshot.Value.CanBeExtendedAfterHowManyMonths))) @if (Model.PlanType != OrganizationPlanType.Free)
{ {
<div class="d-grid gap-2"> @if (currentPlanInfo.IsExtendable && Model.PaidEnrollmentEndDate!.Value.IsBetween(DateTime.Now, DateTime.Now.AddMonths(currentPlanInfo.OnePaidEnrollmentPeriodAsMonth!.Value - currentPlanInfo.CanBeExtendedAfterHowManyMonths!.Value)))
<a href="@Url.Page("/Pricing", new { organizationName = Model.Name })" class="btn btn-info"> {
<i class="fas fa-crown me-2"></i> Extend Now <div class="d-grid gap-2">
</a> <a href="@Url.Page("/Pricing", new { organizationName = Model.Name })" class="btn btn-info">
</div> <i class="fas fa-crown me-2"></i> Extend Now
</a>
</div>
}
else
{
<div class="d-grid gap-2">
<button class="btn btn-info">
<span class="mt-2">@Model.PlanType end date: @Model.PaidEnrollmentEndDate!.Value.ToShortDateString()</span>
</button>
</div>
}
} }
else else
{ {
<div class="d-grid gap-2"> <div class="d-grid gap-2">
<button class="btn btn-info"> <a href="@Url.Page("/Pricing", new { organizationName = Model.Name })" class="btn btn-info">
<span class="mt-2">Premium end date: @Model.PremiumEndDate!.Value.ToShortDateString()</span> <i class="fas fa-crown me-2"></i> Upgrade your organization
</button> </a>
</div> </div>
} }
} }
else else
{ {
<div class="d-grid gap-2"> <abp-card class="text-center">
<a href="@Url.Page("/Pricing", new { organizationName = Model.Name })" class="btn btn-info"> <abp-card-body>
<i class="fas fa-crown me-2"></i> Upgrade To @PremiumPlanInfoOptionsSnapshot.Value.PlanType <span>@Model.PlanType end date: @Model.PaidEnrollmentEndDate!.Value.ToShortDateString()</span>
</a> </abp-card-body>
</div> </abp-card>
} }
} </abp-column>
else </abp-row>
{ </abp-container>
<abp-card class="text-center"> }
<abp-card-body>
<span>Premium end date: @Model.PremiumEndDate!.Value.ToShortDateString()</span>
</abp-card-body>
</abp-card>
}
</abp-column>
</abp-row>
</abp-container>

74
src/EventHub.Web/Pages/Organizations/PlanInfoHelper.cs

@ -0,0 +1,74 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using EventHub.Organizations;
using Volo.Abp;
namespace EventHub.Web.Pages.Organizations;
public static class PlanInfoHelper
{
public static PlanInfoDefinitionDto GetPlan(OrganizationPlanType planType, List<PlanInfoDefinitionDto> plans)
{
return plans.Single(x => x.PlanType == planType);
}
public static string GetProductId(PlanInfoDefinitionDto premiumPlan, bool isExtend)
{
var productIdStringBuilder = new StringBuilder(OrganizationConsts.ProductNamePrefix);
productIdStringBuilder.Append("-");
productIdStringBuilder.Append(premiumPlan.PlanType.ToString());
productIdStringBuilder.Append("-");
if (isExtend)
{
if (!premiumPlan.IsExtendable)
{
throw new BusinessException();
}
productIdStringBuilder.Append("Extend");
}
else
{
productIdStringBuilder.Append("Upgrade");
}
return productIdStringBuilder.ToString();
}
public static string GetProductName(PlanInfoDefinitionDto premiumPlan, string organizationName, bool isExtend)
{
var productNameStringBuilder = new StringBuilder(OrganizationConsts.ProductNamePrefix);
productNameStringBuilder.Append(" ");
productNameStringBuilder.Append(premiumPlan.PlanType.ToString());
productNameStringBuilder.Append(" ");
if (isExtend)
{
if (!premiumPlan.IsExtendable)
{
throw new BusinessException();
}
productNameStringBuilder.Append("Extend");
}
else
{
productNameStringBuilder.Append("Upgrade");
}
productNameStringBuilder.Append(" | ");
productNameStringBuilder.Append(organizationName);
return productNameStringBuilder.ToString();
}
public static string GetFeatureInfoByCount(uint? count)
{
count ??= uint.MaxValue;
return count == uint.MaxValue ? "Unlimited" : count.ToString();
}
}

5
src/EventHub.Web/Pages/Organizations/Profile.cshtml

@ -1,6 +1,7 @@
@page "/organizations/{name}" @page "/organizations/{name}"
@inject IHtmlLocalizer<EventHubResource> L @inject IHtmlLocalizer<EventHubResource> L
@using EventHub.Localization @using EventHub.Localization
@using EventHub.Organizations
@using EventHub.Web @using EventHub.Web
@using EventHub.Web.Pages.Events.Components.EventsArea @using EventHub.Web.Pages.Events.Components.EventsArea
@using EventHub.Web.Pages.Organizations.Components.JoinArea @using EventHub.Web.Pages.Organizations.Components.JoinArea
@ -47,9 +48,9 @@
<div class="col-md-7"> <div class="col-md-7">
<h5 class="detail-head mt-3 font-weight-bold"> <h5 class="detail-head mt-3 font-weight-bold">
@Model.Organization.DisplayName @Model.Organization.DisplayName
@if (Model.Organization.IsPremium) @if (Model.Organization.PlanType != OrganizationPlanType.Free)
{ {
<span class="align-top text-dark font-weight-bold" style="font-size: x-small" abp-badge-pill="Warning"> Premium </span> <span class="align-top text-dark font-weight-bold" style="font-size: x-small" abp-badge-pill="Warning"> @Model.Organization.PlanType </span>
} }
</h5> </h5>
<p> <p>

2
src/EventHub.Web/Pages/Organizations/Profile.js

@ -20,7 +20,7 @@ $(function () {
if (r.extraProperties.Status === "COMPLETED") { if (r.extraProperties.Status === "COMPLETED") {
Swal.fire({ Swal.fire({
title: 'Thank you for your purchase!', title: 'Thank you for your purchase!',
text: 'Your Organization has been upgraded to Premium.', text: 'Your Organization has been upgraded.',
imageUrl: '/assets/success.svg', imageUrl: '/assets/success.svg',
imageHeight: "70", imageHeight: "70",
imageWidth: "70", imageWidth: "70",

14
src/EventHub.Web/Pages/Payment/PreCheckout.cshtml

@ -1,13 +1,13 @@
@page @page
@inherits Payment.Web.Pages.PaymentPageBase @inherits Payment.Web.Pages.PaymentPageBase
@using EventHub.Organizations @using EventHub.Organizations
@using EventHub.Web.PaymentRequests @using EventHub.Web.Pages.Organizations
@using EventHub.Web.Pages.Payment
@using Microsoft.AspNetCore.Http @using Microsoft.AspNetCore.Http
@using Microsoft.Extensions.Options
@using Newtonsoft.Json @using Newtonsoft.Json
@using Payment.Web.Pages.Payment @using Payment.Web.Pages.Payment
@model Payment.Web.Pages.Payment.PreCheckoutPageModel @model Payment.Web.Pages.Payment.PreCheckoutPageModel
@inject IOptionsSnapshot<PremiumPlanInfoOptions> PremiumPlanInfoOptionsSnapshot @inject IOrganizationAppService OrganizationAppService
@{ @{
var isExistExtraProperties = Model.PaymentRequest.ExtraProperties.TryGetValue(nameof(OrganizationPaymentRequestExtraParameterConfiguration), out var ExtraProperties); var isExistExtraProperties = Model.PaymentRequest.ExtraProperties.TryGetValue(nameof(OrganizationPaymentRequestExtraParameterConfiguration), out var ExtraProperties);
@ -22,7 +22,7 @@
throw new BadHttpRequestException(""); throw new BadHttpRequestException("");
} }
var organizationName = organizationPaymentRequestExtraParameterConfiguration.OrganizationName; var organizationName = organizationPaymentRequestExtraParameterConfiguration.OrganizationName;
var planInfo = PremiumPlanInfoOptionsSnapshot.Value; var planInfo = PlanInfoHelper.GetPlan(organizationPaymentRequestExtraParameterConfiguration.TargetPlanType, await OrganizationAppService.GetPlanInfosAsync());
} }
@section scripts { @section scripts {
@ -38,14 +38,14 @@
<div class="card-body px-5 py-4"> <div class="card-body px-5 py-4">
<h2 class="my-4">Product Detail</h2> <h2 class="my-4">Product Detail</h2>
<h3 class="my-4">@OrganizationPlanType.Premium Account: <span class="text-primary">@organizationName.ToUpperInvariant()</span></h3> <h3 class="my-4">@OrganizationPlanType.Premium Account: <span class="text-primary">@organizationName.ToUpperInvariant()</span></h3>
@if (!organizationPaymentRequestExtraParameterConfiguration.IsExtend) @if (!organizationPaymentRequestExtraParameterConfiguration.IsExtend && planInfo.IsExtendable)
{ {
<p>Start Date: <span class="text-dark">@DateTime.Now.ToShortDateString()</span></p> <p>Start Date: <span class="text-dark">@DateTime.Now.ToShortDateString()</span></p>
<p>End Date: <span class="text-dark">@DateTime.Now.AddMonths(planInfo.OnePremiumPeriodAsMonth).ToShortDateString()</span></p> <p>End Date: <span class="text-dark">@DateTime.Now.AddMonths(planInfo.OnePaidEnrollmentPeriodAsMonth!.Value).ToShortDateString()</span></p>
} }
else else
{ {
<p>Your license will be extended for <span class="text-dark">@planInfo.OnePremiumPeriodAsMonth</span> months.</p> <p>Your license will be extended for <span class="text-dark">@planInfo.OnePaidEnrollmentPeriodAsMonth!.Value</span> months.</p>
} }
</div> </div>
</div> </div>

168
src/EventHub.Web/Pages/Pricing.cshtml

@ -1,11 +1,9 @@
@page "/pricing/{organizationName}" @page "/pricing/{organizationName}"
@using EventHub.Localization @using EventHub.Localization
@using EventHub.Web.PaymentRequests @using EventHub.Web.Pages.Organizations
@using Microsoft.AspNetCore.Mvc.Localization @using Microsoft.AspNetCore.Mvc.Localization
@using Microsoft.Extensions.Options
@model EventHub.Web.Pages.Pricing @model EventHub.Web.Pages.Pricing
@inject IHtmlLocalizer<EventHubResource> L @inject IHtmlLocalizer<EventHubResource> L
@inject IOptionsSnapshot<PremiumPlanInfoOptions> PremiumPlanInfoOptionsSnapshot
@section styles { @section styles {
<abp-style src="/Pages/Pricing.css" /> <abp-style src="/Pages/Pricing.css" />
@ -14,89 +12,89 @@
<main class="static-page container mt-5"> <main class="static-page container mt-5">
<h1 class="text-center mb-3">Pricing</h1> <h1 class="text-center mb-3">Pricing</h1>
<p class="text-center mb-5">Simple plans for everyone</p> <p class="text-center mb-5">Simple plans for everyone</p>
<div class="row"> <div class="row">
<div class="col-md-6"> @foreach (var plan in Model.PlanInfos)
<div class="card"> {
<div class="card-body px-4 py-4 text-center"> <div class="col-md-6">
<h2 class="mt-4">Free Account</h2> <div class="card plan-card">
<p class="my-4">For individuals and small organizations.</p> <div class="card-body px-4 py-4 text-center">
<div class="price mb-4"> <h2 class="mt-4">@plan.PlanType Account</h2>
<small>$</small> <p class="my-4">@L[plan.Description]</p>
<span>0</span> <div class="price mb-4">
<small>$</small>
<span>@plan.Price</span>
</div>
<ul class="list-check-icon mb-4 text-start">
<li><i class="fa fa-check-circle"></i> @PlanInfoHelper.GetFeatureInfoByCount(plan.Feature.MaxAllowedEventsCountInOneYear) events in a year. </li>
<li><i class="fa fa-check-circle"></i> Up to @PlanInfoHelper.GetFeatureInfoByCount(plan.Feature.MaxAllowedTracksCountInOneEvent).ToLower() tracks for an event. </li>
<li><i class="fa fa-check-circle"></i> Up to @PlanInfoHelper.GetFeatureInfoByCount(plan.Feature.MaxAllowedAttendeesCountInOneEvent).ToLower() attendees per event. </li>
@foreach (var additionalFeatureInfo in plan.Feature.AdditionalFeatureInfos)
{
<li><i class="fa fa-check-circle"></i> @additionalFeatureInfo </li>
}
</ul>
@if (Model.Organization.PlanType == plan.PlanType && plan.Price > 0 && plan.IsExtendable)
{
<form id="Extend" method="post" asp-page-handler="Extend">
<abp-input asp-for="TargetPlanToUpgrade" value="@plan.PlanType"></abp-input>
<abp-input asp-for="OrganizationName"></abp-input>
<a onclick="document.getElementById('Extend').submit()" class="btn btn-info my-4"><i class="fas fa-crown me-2"></i> Extend Now</a>
</form>
}
else if (plan.Price > 0)
{
<form id="Upgrade-@plan.PlanType" method="post" asp-page-handler="Upgrade">
<abp-input asp-for="TargetPlanToUpgrade" value="@plan.PlanType"></abp-input>
<abp-input asp-for="OrganizationName"></abp-input>
<a onclick="document.getElementById('Upgrade-@plan.PlanType').submit()" class="btn btn-info my-4"><i class="fas fa-crown me-2"></i> Upgrade to @plan.PlanType</a>
</form>
}
</div> </div>
<ul class="list-check-icon mb-4 text-start"> </div>
<li><i class="fa fa-check-circle"></i> 2 events in a year. </li> </div>
<li><i class="fa fa-check-circle"></i> Up to 2 tracks for an event. </li> }
<li><i class="fa fa-check-circle"></i> Up to 1,000 attendees per event. </li>
</ul>
</div>
</div>
</div>
@if (Model.PremiumPlanInfo is not null)
{
<div class="col-md-6">
<div class="card">
<div class="card-body px-4 py-4 text-center">
<h2 class="mt-4">Premium Account</h2>
<p class="my-4">For those organize regular events.</p>
<div class="price mb-4">
<small>$</small>
<span>@Model.PremiumPlanInfo.Price</span>
</div>
<ul class="list-check-icon mb-4 text-start">
<li><i class="fa fa-check-circle"></i> Unlimited events. </li>
<li><i class="fa fa-check-circle"></i> Unlimited tracks and sessions for an event. </li>
<li><i class="fa fa-check-circle"></i> Unlimited attendees. </li>
<li><i class="fa fa-check-circle"></i> Be more prominent in the organization list. </li>
<li><i class="fa fa-check-circle"></i> Premium badge in the organization page. </li>
</ul>
@if (Model.Organization.IsPremium)
{
<form id="Extend" method="post" asp-page-handler="Extend">
<abp-input asp-for="TargetPlanToUpgrade" value="@Model.PremiumPlanInfo.PlanType"></abp-input>
<abp-input asp-for="OrganizationName"></abp-input>
<a onclick="document.getElementById('Extend').submit()" class="btn btn-info my-4"><i class="fas fa-crown me-2"></i> Extend Now</a>
</form>
}
else
{
<form id="Upgrade" method="post" asp-page-handler="Upgrade">
<abp-input asp-for="TargetPlanToUpgrade" value="@Model.PremiumPlanInfo.PlanType"></abp-input>
<abp-input asp-for="OrganizationName"></abp-input>
<a onclick="document.getElementById('Upgrade').submit()" class="btn btn-info my-4"><i class="fas fa-crown me-2"></i> Upgrade to @Model.PremiumPlanInfo.PlanType</a>
</form>
}
</div>
</div>
</div>
}
<h2 class="text-center mb-3">Frequently Asked Questions</h2> <h2 class="text-center mb-3">Frequently Asked Questions</h2>
<div id="accordion" class="mb-5 pb-5 faq-container"> <div id="accordion" class="mb-5 pb-5 faq-container">
<div class="faq" data-anchor-id="accordion12"> <div class="faq" data-anchor-id="accordion12">
<div class="faq-header"> <div class="faq-header">
<h5><button class="btn btn-faq collapsed" data-bs-toggle="collapse" data-bs-target="#accordion12" aria-expanded="false" aria-controls="accordion12">How do I renew my plan?<i class="fas fa-chevron-up"></i> </button></h5> <h5><button class="btn btn-faq collapsed" data-bs-toggle="collapse" data-bs-target="#accordion12" aria-expanded="false" aria-controls="accordion12">How do I renew my plan?<i class="fas fa-chevron-up"></i> </button></h5>
</div> </div>
<div id="accordion12" class="collapse" data-bs-parent="#accordion" style=""> <div id="accordion12" class="collapse" data-bs-parent="#accordion" style="">
<div class="faq-body"> <div class="faq-body">
@{ @foreach (var plan in Model.PlanInfos.Where(x => x.Price > 0))
var canExtendBeforeHowManyMonths = PremiumPlanInfoOptionsSnapshot.Value.OnePremiumPeriodAsMonth - PremiumPlanInfoOptionsSnapshot.Value.CanBeExtendedAfterHowManyMonths; {
} @if (plan.IsExtendable)
<p>@(canExtendBeforeHowManyMonths == PremiumPlanInfoOptionsSnapshot.Value.OnePremiumPeriodAsMonth ? "You " : "In the last " + canExtendBeforeHowManyMonths + " months of your premium period, you") can renew your plan by going to the Organization detail page. {
</p> var canExtendBeforeHowManyMonths = plan.OnePaidEnrollmentPeriodAsMonth!.Value - plan.CanBeExtendedAfterHowManyMonths!.Value;
</div> @if (canExtendBeforeHowManyMonths != 0)
</div> {
</div> <p>For @plan.PlanType, @(canExtendBeforeHowManyMonths == plan.OnePaidEnrollmentPeriodAsMonth!.Value ? "You " : "in the last " + canExtendBeforeHowManyMonths + " months of your " + plan.PlanType.ToString().ToLower() + " period, you") can renew your plan by going to the organization detail page.</p>
<div class="faq" data-anchor-id="accordion123"> }
<div class="faq-header"> else
<h5><button class="btn btn-faq collapsed" data-bs-toggle="collapse" data-bs-target="#accordion123" aria-expanded="false" aria-controls="accordion123">What happens when my license period ends?<i class="fas fa-chevron-up"></i> </button></h5> {
</div> <p>For @plan.PlanType, when your @plan.PlanType period expires, you can repurchase your plan by going to the organization detail page.</p>
<div id="accordion123" class="collapse" data-bs-parent="#accordion" style=""> }
<div class="faq-body"> }
<p>Don't worry about even if your organization has run out of premium, you can upgrade back to the Premium plan at any time. else
</p> {
</div> <p>For @plan.PlanType, when your @plan.PlanType period is over, you can buy again your plan by going to the Organization detail page.</p>
</div> }
</div>
</div> }
</div>
</div>
</div>
<div class="faq" data-anchor-id="accordion123">
<div class="faq-header">
<h5><button class="btn btn-faq collapsed" data-bs-toggle="collapse" data-bs-target="#accordion123" aria-expanded="false" aria-controls="accordion123">What happens when my license period ends?<i class="fas fa-chevron-up"></i> </button></h5>
</div>
<div id="accordion123" class="collapse" data-bs-parent="#accordion" style="">
<div class="faq-body">
<p>Don't worry about even if your organization has run out of premium, you can upgrade back to the plan at any time.
</p>
</div>
</div>
</div>
</div>
</div> </div>
</main> </main>

41
src/EventHub.Web/Pages/Pricing.cshtml.cs

@ -1,9 +1,9 @@
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using EventHub.Organizations; using EventHub.Organizations;
using EventHub.Web.PaymentRequests; using EventHub.Web.Pages.Organizations;
using JetBrains.Annotations; using JetBrains.Annotations;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Payment.PaymentRequests; using Payment.PaymentRequests;
using Payment.Web.PaymentRequest; using Payment.Web.PaymentRequest;
using Volo.Abp.Authorization; using Volo.Abp.Authorization;
@ -21,26 +21,24 @@ namespace EventHub.Web.Pages
[BindProperty] [BindProperty]
public OrganizationPlanType TargetPlanToUpgrade { get; set; } public OrganizationPlanType TargetPlanToUpgrade { get; set; }
[CanBeNull] [ItemNotNull]
public PremiumPlanInfoOptions PremiumPlanInfo { get; private set; } public List<PlanInfoDefinitionDto> PlanInfos { get; private set; }
public OrganizationProfileDto Organization { get; private set; } public OrganizationProfileDto Organization { get; private set; }
private readonly IOrganizationAppService _organizationAppService; private readonly IOrganizationAppService _organizationAppService;
private readonly IPaymentRequestAppService _paymentRequestAppService; private readonly IPaymentRequestAppService _paymentRequestAppService;
private readonly IPaymentUrlBuilder _paymentUrlBuilder; private readonly IPaymentUrlBuilder _paymentUrlBuilder;
private readonly IOptionsSnapshot<PremiumPlanInfoOptions> _premiumPlanInfoOptionsSnapshot;
public Pricing( public Pricing(
IOrganizationAppService organizationAppService, IOrganizationAppService organizationAppService,
IPaymentRequestAppService paymentRequestAppService, IPaymentRequestAppService paymentRequestAppService,
IPaymentUrlBuilder paymentUrlBuilder, IPaymentUrlBuilder paymentUrlBuilder)
IOptionsSnapshot<PremiumPlanInfoOptions> premiumPlanInfoOptionsSnapshot)
{ {
_organizationAppService = organizationAppService; _organizationAppService = organizationAppService;
_paymentRequestAppService = paymentRequestAppService; _paymentRequestAppService = paymentRequestAppService;
_paymentUrlBuilder = paymentUrlBuilder; _paymentUrlBuilder = paymentUrlBuilder;
_premiumPlanInfoOptionsSnapshot = premiumPlanInfoOptionsSnapshot; PlanInfos = new List<PlanInfoDefinitionDto>();
} }
public async Task OnGetAsync() public async Task OnGetAsync()
@ -50,19 +48,15 @@ namespace EventHub.Web.Pages
{ {
throw new AbpAuthorizationException(); throw new AbpAuthorizationException();
} }
if (_premiumPlanInfoOptionsSnapshot.Value.IsActive)
{
PremiumPlanInfo = _premiumPlanInfoOptionsSnapshot.Value;
}
PlanInfos = await _organizationAppService.GetPlanInfosAsync();
} }
public async Task<IActionResult> OnPostUpgradeAsync() public async Task<IActionResult> OnPostUpgradeAsync()
{ {
var organization = await GetOrganizationProfileAsync(); var organization = await GetOrganizationProfileAsync();
PlanInfos = await _organizationAppService.GetPlanInfosAsync();
var plan = _premiumPlanInfoOptionsSnapshot.Value; var plan = PlanInfoHelper.GetPlan(TargetPlanToUpgrade, PlanInfos);
if (plan is null || !plan.IsActive) if (plan is null || !plan.IsActive)
{ {
Alerts.Danger("Premium plan is currently inactive!"); Alerts.Danger("Premium plan is currently inactive!");
@ -78,7 +72,9 @@ namespace EventHub.Web.Pages
{ {
var organization = await GetOrganizationProfileAsync(); var organization = await GetOrganizationProfileAsync();
var plan = _premiumPlanInfoOptionsSnapshot.Value; PlanInfos = await _organizationAppService.GetPlanInfosAsync();
var plan = PlanInfoHelper.GetPlan(TargetPlanToUpgrade, PlanInfos);
if (plan is null || !plan.IsActive) if (plan is null || !plan.IsActive)
{ {
Alerts.Danger("Premium plan is currently inactive!"); Alerts.Danger("Premium plan is currently inactive!");
@ -102,7 +98,7 @@ namespace EventHub.Web.Pages
} }
private async Task<PaymentRequestDto> CreatePaymentRequestAsync( private async Task<PaymentRequestDto> CreatePaymentRequestAsync(
PremiumPlanInfoOptions plan, PlanInfoDefinitionDto plan,
OrganizationProfileDto organization, OrganizationProfileDto organization,
bool isExtend = false) bool isExtend = false)
{ {
@ -110,8 +106,8 @@ namespace EventHub.Web.Pages
{ {
CustomerId = CurrentUser.GetId().ToString(), CustomerId = CurrentUser.GetId().ToString(),
Price = plan.Price, Price = plan.Price,
ProductId = PremiumPlanInfoOptions.GetProductId(plan, isExtend), ProductId = PlanInfoHelper.GetProductId(plan, isExtend),
ProductName = PremiumPlanInfoOptions.GetProductName(plan, organization.Name, isExtend), ProductName = PlanInfoHelper.GetProductName(plan, organization.Name, isExtend),
ExtraProperties = ExtraProperties =
{ {
{ {
@ -119,8 +115,9 @@ namespace EventHub.Web.Pages
new OrganizationPaymentRequestExtraParameterConfiguration new OrganizationPaymentRequestExtraParameterConfiguration
{ {
OrganizationName = OrganizationName, OrganizationName = OrganizationName,
PremiumPeriodAsMonth = plan.OnePremiumPeriodAsMonth, PremiumPeriodAsMonth = plan.OnePaidEnrollmentPeriodAsMonth,
IsExtend = isExtend IsExtend = isExtend,
TargetPlanType = TargetPlanToUpgrade
} }
} }
} }
@ -129,4 +126,4 @@ namespace EventHub.Web.Pages
return paymentRequest; return paymentRequest;
} }
} }
} }

6
src/EventHub.Web/Pages/Pricing.css

@ -36,6 +36,10 @@
overflow: hidden; overflow: hidden;
} }
.plan-card {
height: 570px;
}
html:not([dir=rtl]) .mb-4, html:not([dir=rtl]) .my-4 { html:not([dir=rtl]) .mb-4, html:not([dir=rtl]) .my-4 {
margin-bottom: 1.5rem !important; margin-bottom: 1.5rem !important;
} }

85
src/EventHub.Web/PaymentRequests/PremiumPlanInfoOptions.cs

@ -1,85 +0,0 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Text;
using EventHub.Organizations;
using Volo.Abp;
namespace EventHub.Web.PaymentRequests
{
public class PremiumPlanInfoOptions
{
private const string ProductNamePrefix = "EventHub";
public const string OrganizationPlanInfo = "PlanInfo:Premium";
public OrganizationPlanType PlanType { get; private set; } = OrganizationPlanType.Premium;
[DefaultValue(false)]
public bool IsActive { get; set; }
[Range(1.0, 100.0, ErrorMessage = "Value for {0} must be between {1} and {2}.")]
public decimal Price { get; set; } = 1.0M;
[DefaultValue(false)]
public bool IsExtendable { get; set; }
[Range(0, 12, ErrorMessage = "Value for {0} must be between {1} and {2}.")]
public int CanBeExtendedAfterHowManyMonths { get; set; }
[Range(1, 24, ErrorMessage = "Value for {0} must be between {1} and {2}.")]
public int OnePremiumPeriodAsMonth { get; set; } = 12;
public static string GetProductId(PremiumPlanInfoOptions premiumPlan, bool isExtend)
{
var productIdStringBuilder = new StringBuilder(ProductNamePrefix);
productIdStringBuilder.Append("-");
productIdStringBuilder.Append(premiumPlan.PlanType.ToString());
productIdStringBuilder.Append("-");
if (isExtend)
{
if (!premiumPlan.IsExtendable)
{
throw new BusinessException();
}
productIdStringBuilder.Append("Extend");
}
else
{
productIdStringBuilder.Append("Upgrade");
}
return productIdStringBuilder.ToString();
}
public static string GetProductName(PremiumPlanInfoOptions premiumPlan, string organizationName, bool isExtend)
{
var productNameStringBuilder = new StringBuilder(ProductNamePrefix);
productNameStringBuilder.Append(" ");
productNameStringBuilder.Append(premiumPlan.PlanType.ToString());
productNameStringBuilder.Append(" ");
if (isExtend)
{
if (!premiumPlan.IsExtendable)
{
throw new BusinessException();
}
productNameStringBuilder.Append("Extend");
}
else
{
productNameStringBuilder.Append("Upgrade");
}
productNameStringBuilder.Append(" | ");
productNameStringBuilder.Append(organizationName);
return productNameStringBuilder.ToString();
}
}
}

9
src/EventHub.Web/appsettings.json

@ -7,14 +7,5 @@
"RequireHttpsMetadata": "true", "RequireHttpsMetadata": "true",
"ClientId": "EventHub_Web", "ClientId": "EventHub_Web",
"ClientSecret": "1q2w3e*" "ClientSecret": "1q2w3e*"
},
"PlanInfo": {
"Premium": {
"IsActive": true,
"Price": 29.90,
"IsExtendable": true,
"CanBeExtendedAfterHowManyMonths": 9,
"OnePremiumPeriodAsMonth": 12
}
} }
} }

20
test/EventHub.Domain.Tests/Events/Registrations/EventRegistrationManager_UnitTests.cs

@ -1,5 +1,6 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using EventHub.Organizations.Plans;
using NSubstitute; using NSubstitute;
using Shouldly; using Shouldly;
using Volo.Abp.Guids; using Volo.Abp.Guids;
@ -18,10 +19,10 @@ namespace EventHub.Events.Registrations
clock.Now.Returns(DateTime.Now); clock.Now.Returns(DateTime.Now);
var registrationManager = new EventRegistrationManager( var registrationManager = new EventRegistrationManager(
null, null, clock null, null, clock, null
); );
var evnt = new Event( var @event = new Event(
Guid.NewGuid(), Guid.NewGuid(),
Guid.NewGuid(), Guid.NewGuid(),
"1a8j3v0d", "1a8j3v0d",
@ -31,13 +32,13 @@ namespace EventHub.Events.Registrations
"In this event, we will introduce the ABP Framework..." "In this event, we will introduce the ABP Framework..."
); );
registrationManager.IsPastEvent(evnt).ShouldBeTrue(); registrationManager.IsPastEvent(@event).ShouldBeTrue();
} }
[Fact] [Fact]
public async Task RegisterAsync() public async Task RegisterAsync()
{ {
var evnt = new Event( var @event = new Event(
Guid.NewGuid(), Guid.NewGuid(),
Guid.NewGuid(), Guid.NewGuid(),
"1a8j3v0d", "1a8j3v0d",
@ -50,23 +51,26 @@ namespace EventHub.Events.Registrations
var user = new IdentityUser(Guid.NewGuid(), "john", "john@abp.io"); var user = new IdentityUser(Guid.NewGuid(), "john", "john@abp.io");
var repository = Substitute.For<IEventRegistrationRepository>(); var repository = Substitute.For<IEventRegistrationRepository>();
repository.ExistsAsync(evnt.Id, user.Id).Returns(Task.FromResult(false)); repository.ExistsAsync(@event.Id, user.Id).Returns(Task.FromResult(false));
var guidGenerator = SimpleGuidGenerator.Instance; var guidGenerator = SimpleGuidGenerator.Instance;
var clock = Substitute.For<IClock>(); var clock = Substitute.For<IClock>();
clock.Now.Returns(DateTime.Now); clock.Now.Returns(DateTime.Now);
var planFeatureManager = Substitute.For<PlanFeatureManager>(null, null, null, null, null);
planFeatureManager.CanRegisterToEventAsync(@event).Returns(Task.FromResult(true));
var registrationManager = new EventRegistrationManager( var registrationManager = new EventRegistrationManager(
repository, guidGenerator, clock repository, guidGenerator, clock, planFeatureManager
); );
await registrationManager.RegisterAsync(evnt, user); await registrationManager.RegisterAsync(@event, user);
await repository await repository
.Received() .Received()
.InsertAsync( .InsertAsync(
Arg.Is<EventRegistration>(er => er.EventId == evnt.Id && er.UserId == user.Id) Arg.Is<EventRegistration>(er => er.EventId == @event.Id && er.UserId == user.Id)
); );
} }
} }

14
test/EventHub.TestBase/EventHubTestBaseModule.cs

@ -1,4 +1,6 @@
using System; using System;
using EventHub.Organizations;
using EventHub.Organizations.Plans;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Volo.Abp; using Volo.Abp;
using Volo.Abp.Authorization; using Volo.Abp.Authorization;
@ -40,6 +42,18 @@ namespace EventHub
}); });
context.Services.AddAlwaysAllowAuthorization(); context.Services.AddAlwaysAllowAuthorization();
Configure<PlanInfoOptions>(options =>
{
options.Infos.Add(new PlanInfoDefinition
{
PlanType = OrganizationPlanType.Free,
Description = "This is a default plan.",
IsActive = true,
Price = 0,
IsExtendable = false
});
});
} }
public override void OnApplicationInitialization(ApplicationInitializationContext context) public override void OnApplicationInitialization(ApplicationInitializationContext context)

Loading…
Cancel
Save