Browse Source

feat: set up extensible organization upgrade system

pull/90/head
berkansasmaz 5 years ago
parent
commit
045a652cdc
No known key found for this signature in database GPG Key ID: 884D815C3F32BE00
  1. 19
      src/EventHub.Application.Contracts/Organizations/FeatureOfPlanDefinitionDto.cs
  2. 3
      src/EventHub.Application.Contracts/Organizations/IOrganizationAppService.cs
  3. 25
      src/EventHub.Application.Contracts/Organizations/PlanInfoDefinitionDto.cs
  4. 4
      src/EventHub.Application/EventHubApplicationAutoMapperProfile.cs
  5. 15
      src/EventHub.Application/Organizations/OrganizationAppService.cs
  6. 2
      src/EventHub.Domain.Shared/Organizations/OrganizationContst.cs
  7. 6
      src/EventHub.Domain.Shared/Organizations/OrganizationPaymentRequestExtraParameterConfiguration.cs
  8. 5
      src/EventHub.Domain.Shared/Organizations/OrganizationPlanType.cs
  9. 23
      src/EventHub.Domain/Organizations/PaymentRequests/FeatureOfPlanDefinition.cs
  10. 9
      src/EventHub.Domain/Organizations/PaymentRequests/IPlanInfoDefinitionStore.cs
  11. 6
      src/EventHub.Domain/Organizations/PaymentRequests/PaymentRequestEventHandler.cs
  12. 37
      src/EventHub.Domain/Organizations/PaymentRequests/PlanInfoDefinition.cs
  13. 21
      src/EventHub.Domain/Organizations/PaymentRequests/PlanInfoDefinitionStore.cs
  14. 30
      src/EventHub.Domain/Organizations/PaymentRequests/PlanInfoOptions.cs
  15. 45
      src/EventHub.HttpApi.Host/EventHubHttpApiHostModule.cs
  16. 31
      src/EventHub.HttpApi.Host/appsettings.json
  17. 8
      src/EventHub.HttpApi/Controllers/Organizations/OrganizationController.cs
  18. 18
      src/EventHub.Web/EventHubWebModule.cs
  19. 17
      src/EventHub.Web/Pages/Organizations/Components/_upgradeOrExtendOrganizationSection.cshtml
  20. 73
      src/EventHub.Web/Pages/Payment/PlanInfoHelper.cs
  21. 13
      src/EventHub.Web/Pages/Payment/PreCheckout.cshtml
  22. 161
      src/EventHub.Web/Pages/Pricing.cshtml
  23. 42
      src/EventHub.Web/Pages/Pricing.cshtml.cs
  24. 85
      src/EventHub.Web/PaymentRequests/PremiumPlanInfoOptions.cs
  25. 9
      src/EventHub.Web/appsettings.json

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

@ -0,0 +1,19 @@
using System.Collections.Generic;
namespace EventHub.Organizations;
public class FeatureOfPlanDefinitionDto
{
public uint? MaxAllowedEventsCount { 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.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
@ -21,5 +22,7 @@ namespace EventHub.Organizations
Task UpdateAsync(Guid id, UpdateOrganizationDto input);
Task<IRemoteStreamContent> GetProfilePictureAsync(Guid id);
Task<List<PlanInfoDefinitionDto>> GetPlanInfosAsync();
}
}

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? OnePremiumPeriodAsMonth { 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.Organizations;
using EventHub.Organizations.Memberships;
using EventHub.Organizations.PaymentRequests;
using EventHub.Users;
using Volo.Abp.AutoMapper;
using Volo.Abp.Identity;
@ -48,6 +49,9 @@ namespace EventHub
CreateMap<IdentityUser, UserDto>();
CreateMap<UserWithoutDetails, UserInListDto>();
CreateMap<PlanInfoDefinition, PlanInfoDefinitionDto>();
CreateMap<FeatureOfPlanDefinition, FeatureOfPlanDefinitionDto>();
}
}
}

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

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using EventHub.Organizations.Memberships;
using EventHub.Organizations.PaymentRequests;
using EventHub.Users;
using Microsoft.AspNetCore.Authorization;
using Volo.Abp.Application.Dtos;
@ -21,19 +22,22 @@ namespace EventHub.Organizations
private readonly OrganizationManager _organizationManager;
private readonly IBlobContainer<OrganizationProfilePictureContainer> _organizationBlobContainer;
private readonly IUserRepository _userRepository;
private readonly IPlanInfoDefinitionStore _planInfoDefinitionStore;
public OrganizationAppService(
IRepository<Organization, Guid> organizationRepository,
IOrganizationMembershipRepository organizationMembershipsRepository,
OrganizationManager organizationManager,
IBlobContainer<OrganizationProfilePictureContainer> organizationBlobContainer,
IUserRepository userRepository)
IUserRepository userRepository,
IPlanInfoDefinitionStore planInfoDefinitionStore)
{
_organizationRepository = organizationRepository;
_organizationMembershipsRepository = organizationMembershipsRepository;
_organizationManager = organizationManager;
_organizationBlobContainer = organizationBlobContainer;
_userRepository = userRepository;
_planInfoDefinitionStore = planInfoDefinitionStore;
}
[Authorize]
@ -163,6 +167,13 @@ namespace EventHub.Organizations
return new RemoteStreamContent(pictureContent, blobName);
}
[Authorize]
public async Task<List<PlanInfoDefinitionDto>> GetPlanInfosAsync()
{
var dd = await _planInfoDefinitionStore.GetPlanInfosAsync();
return ObjectMapper.Map<List<PlanInfoDefinition>, List<PlanInfoDefinitionDto>>(dd);
}
private async Task SaveProfilePictureAsync(Guid id, IRemoteStreamContent streamContent)
{
var blobName = id.ToString();
@ -170,4 +181,4 @@ namespace EventHub.Organizations
await _organizationBlobContainer.SaveAsync(blobName, streamContent.GetStream(), overrideExisting: true);
}
}
}
}

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

@ -27,6 +27,8 @@
public const int MaxMediumUsernameLength = 24;
public const string ProductNamePrefix = "EventHub";
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 int PremiumPeriodAsMonth { get; set; }
public int? PremiumPeriodAsMonth { get; set; }
public OrganizationPlanType TargetPlanType { get; set; }
}
}
}

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

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

23
src/EventHub.Domain/Organizations/PaymentRequests/FeatureOfPlanDefinition.cs

@ -0,0 +1,23 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using JetBrains.Annotations;
namespace EventHub.Organizations.PaymentRequests;
public class FeatureOfPlanDefinition
{
[CanBeNull]
[Range(0, uint.MaxValue, ErrorMessage = "Value for {0} must be between {1} and {2}.")]
public uint? MaxAllowedEventsCount { get; set; }
[CanBeNull]
[Range(0, uint.MaxValue, ErrorMessage = "Value for {0} must be between {1} and {2}.")]
public uint? MaxAllowedTracksCountInOneEvent { get; set; }
[CanBeNull]
[Range(0, uint.MaxValue, ErrorMessage = "Value for {0} must be between {1} and {2}.")]
public uint? MaxAllowedAttendeesCountInOneEvent { get; set; }
[NotNull]
public List<string> AdditionalFeatureInfos { get; set; }
}

9
src/EventHub.Domain/Organizations/PaymentRequests/IPlanInfoDefinitionStore.cs

@ -0,0 +1,9 @@
using System.Collections.Generic;
using System.Threading.Tasks;
namespace EventHub.Organizations.PaymentRequests;
public interface IPlanInfoDefinitionStore
{
Task<List<PlanInfoDefinition>> GetPlanInfosAsync();
}

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

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

37
src/EventHub.Domain/Organizations/PaymentRequests/PlanInfoDefinition.cs

@ -0,0 +1,37 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using JetBrains.Annotations;
namespace EventHub.Organizations.PaymentRequests;
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, ErrorMessage = "Value for {0} must be between {1} and {2}.")]
public decimal Price { get; set; } = 1.0M;
public bool IsExtendable { get; set; }
[CanBeNull]
[Range(0, 12, ErrorMessage = "Value for {0} must be between {1} and {2}.")]
public int? CanBeExtendedAfterHowManyMonths { get; set; }
[CanBeNull]
[Range(1, 24, ErrorMessage = "Value for {0} must be between {1} and {2}.")]
public int? OnePremiumPeriodAsMonth { get; set; } = 12;
public FeatureOfPlanDefinition Feature { get; set; }
public PlanInfoDefinition()
{
Feature = new FeatureOfPlanDefinition();
Feature.AdditionalFeatureInfos = new List<string>();
}
}

21
src/EventHub.Domain/Organizations/PaymentRequests/PlanInfoDefinitionStore.cs

@ -0,0 +1,21 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
namespace EventHub.Organizations.PaymentRequests;
public class PlanInfoDefinitionStore : IPlanInfoDefinitionStore, ITransientDependency
{
protected PlanInfoOptions PlanInfoOptions { get; }
public PlanInfoDefinitionStore(IOptions<PlanInfoOptions> planInfoOptions)
{
PlanInfoOptions = planInfoOptions.Value;
}
public Task<List<PlanInfoDefinition>> GetPlanInfosAsync()
{
return Task.FromResult(PlanInfoOptions.Infos);
}
}

30
src/EventHub.Domain/Organizations/PaymentRequests/PlanInfoOptions.cs

@ -0,0 +1,30 @@
using System.Collections.Generic;
namespace EventHub.Organizations.PaymentRequests;
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;
}
}

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

@ -2,9 +2,11 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using EventHub.EntityFrameworkCore;
using EventHub.Events;
using EventHub.Organizations;
using EventHub.Organizations.PaymentRequests;
using EventHub.Utils;
using EventHub.Web;
using Microsoft.AspNetCore.Authentication.JwtBearer;
@ -15,6 +17,7 @@ using Microsoft.AspNetCore.Localization;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;
using StackExchange.Redis;
using Volo.Abp;
@ -63,6 +66,7 @@ namespace EventHub
ConfigureBackgroundJobs();
ConfigureAutoApiControllers();
ConfigureTiming();
ConfigurePremiumPlanInfo(context, configuration);
}
private void ConfigureAutoApiControllers()
@ -190,6 +194,47 @@ namespace EventHub
{
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(configOfDefinitions =>
{
foreach (var config in configOfDefinitions)
{
var isValidPlanType = Enum.IsDefined(typeof(OrganizationPlanType), config.PlanType);
if (!isValidPlanType)
{
return false;
}
var isExistSamePlan = configOfDefinitions.Count(x =>
x.Price == config.Price && x.PlanType == config.PlanType);
if (isExistSamePlan > 1)
{
return false;
}
if (config.IsActive && config.IsExtendable)
{
Check.NotNull(config.OnePremiumPeriodAsMonth, nameof(config.OnePremiumPeriodAsMonth));
Check.NotNull(config.CanBeExtendedAfterHowManyMonths, nameof(config.CanBeExtendedAfterHowManyMonths));
return config.OnePremiumPeriodAsMonth > config.CanBeExtendedAfterHowManyMonths;
}
}
return true;
}, "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)
{

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

@ -18,5 +18,34 @@
"Secret": "PAYPAL_SECRET",
"Environment": "Sandbox"
}
}
},
"PlanInfos": [
{
"PlanType": "Free",
"Description": "For individuals and small organizations.",
"IsActive": true,
"Price": 0,
"IsExtendable": false,
"Feature": {
"MaxAllowedEventsCount": 2,
"MaxAllowedTracksCountInOneEvent": 2,
"MaxAllowedAttendeesCountInOneEvent": 1000
}
},
{
"PlanType": "Premium",
"Description": "For those organize regular events.",
"IsActive": true,
"Price": 29.90,
"IsExtendable": true,
"CanBeExtendedAfterHowManyMonths": 9,
"OnePremiumPeriodAsMonth": 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.Collections.Generic;
using System.Threading.Tasks;
using EventHub.Organizations;
using Microsoft.AspNetCore.Mvc;
@ -84,5 +85,12 @@ namespace EventHub.Controllers.Organizations
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 EventHub.Localization;
using EventHub.Web.Menus;
using EventHub.Web.PaymentRequests;
using EventHub.Web.Theme;
using EventHub.Web.Theme.Bundling;
using EventHub.Web.Utils;
@ -86,7 +85,6 @@ namespace EventHub.Web
ConfigureCookies(context);
ConfigureSwaggerServices(context.Services);
ConfigureRazorPageOptions();
ConfigurePremiumPlanInfo(context, configuration);
}
private void ConfigureBundles()
@ -233,22 +231,6 @@ namespace EventHub.Web
.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)
{
var app = context.GetApplicationBuilder();

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

@ -1,11 +1,14 @@
@using EventHub.Web.PaymentRequests
@using Microsoft.Extensions.Options
@using EventHub.Organizations
@model EventHub.Organizations.OrganizationProfileDto
@inject IOptionsSnapshot<PremiumPlanInfoOptions> PremiumPlanInfoOptionsSnapshot
@inject IOrganizationAppService OrganizationAppService
@{
var premiumPlanInfo = (await OrganizationAppService.GetPlanInfosAsync()).MaxBy(x => x.Price)!;
}
<abp-container class="mt-4">
<abp-row>
@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.IsPremium && premiumPlanInfo.IsActive && premiumPlanInfo.IsExtendable && Model.PremiumEndDate!.Value.IsBetween(DateTime.Now, DateTime.Now.AddMonths(premiumPlanInfo.OnePremiumPeriodAsMonth!.Value - premiumPlanInfo.CanBeExtendedAfterHowManyMonths!.Value)))
{
<span class="mb-2 ms-5 text-center strong">Premium end date: @Model.PremiumEndDate!.Value.ToShortDateString()</span>
}
@ -13,11 +16,11 @@
<a href="/organization/edit/@Model.Name" class="btn btn-success text-white">Edit</a>
</abp-column>
<abp-column size="_9">
@if (PremiumPlanInfoOptionsSnapshot.Value.IsActive)
@if (premiumPlanInfo.IsActive)
{
@if (Model.IsPremium)
{
@if (PremiumPlanInfoOptionsSnapshot.Value.IsExtendable && Model.PremiumEndDate!.Value.IsBetween(DateTime.Now, DateTime.Now.AddMonths(PremiumPlanInfoOptionsSnapshot.Value.OnePremiumPeriodAsMonth - PremiumPlanInfoOptionsSnapshot.Value.CanBeExtendedAfterHowManyMonths)))
@if (premiumPlanInfo.IsExtendable && Model.PremiumEndDate!.Value.IsBetween(DateTime.Now, DateTime.Now.AddMonths(premiumPlanInfo.OnePremiumPeriodAsMonth!.Value - premiumPlanInfo.CanBeExtendedAfterHowManyMonths!.Value)))
{
<div class="d-grid gap-2">
<a href="@Url.Page("/Pricing", new { organizationName = Model.Name })" class="btn btn-info">
@ -38,7 +41,7 @@
{
<div class="d-grid gap-2">
<a href="@Url.Page("/Pricing", new { organizationName = Model.Name })" class="btn btn-info">
<i class="fas fa-crown me-2"></i> Upgrade To @PremiumPlanInfoOptionsSnapshot.Value.PlanType
<i class="fas fa-crown me-2"></i> Upgrade To @premiumPlanInfo.PlanType
</a>
</div>
}

73
src/EventHub.Web/Pages/Payment/PlanInfoHelper.cs

@ -0,0 +1,73 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using EventHub.Organizations;
using Volo.Abp;
namespace EventHub.Web.Pages.Payment;
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 GetRestrictionInfoByCount(uint? count)
{
return count is null ? "Unlimited" : count.ToString();
}
}

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

@ -1,13 +1,12 @@
@page
@inherits Payment.Web.Pages.PaymentPageBase
@using EventHub.Organizations
@using EventHub.Web.PaymentRequests
@using EventHub.Web.Pages.Payment
@using Microsoft.AspNetCore.Http
@using Microsoft.Extensions.Options
@using Newtonsoft.Json
@using Payment.Web.Pages.Payment
@model Payment.Web.Pages.Payment.PreCheckoutPageModel
@inject IOptionsSnapshot<PremiumPlanInfoOptions> PremiumPlanInfoOptionsSnapshot
@inject IOrganizationAppService OrganizationAppService
@{
var isExistExtraProperties = Model.PaymentRequest.ExtraProperties.TryGetValue(nameof(OrganizationPaymentRequestExtraParameterConfiguration), out var ExtraProperties);
@ -22,7 +21,7 @@
throw new BadHttpRequestException("");
}
var organizationName = organizationPaymentRequestExtraParameterConfiguration.OrganizationName;
var planInfo = PremiumPlanInfoOptionsSnapshot.Value;
var planInfo = PlanInfoHelper.GetPlan(organizationPaymentRequestExtraParameterConfiguration.TargetPlanType, await OrganizationAppService.GetPlanInfosAsync());
}
@section scripts {
@ -38,14 +37,14 @@
<div class="card-body px-5 py-4">
<h2 class="my-4">Product Detail</h2>
<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>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.OnePremiumPeriodAsMonth!.Value).ToShortDateString()</span></p>
}
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.OnePremiumPeriodAsMonth!.Value</span> months.</p>
}
</div>
</div>

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

@ -1,11 +1,9 @@
@page "/pricing/{organizationName}"
@using EventHub.Localization
@using EventHub.Web.PaymentRequests
@using EventHub.Web.Pages.Payment
@using Microsoft.AspNetCore.Mvc.Localization
@using Microsoft.Extensions.Options
@model EventHub.Web.Pages.Pricing
@inject IHtmlLocalizer<EventHubResource> L
@inject IOptionsSnapshot<PremiumPlanInfoOptions> PremiumPlanInfoOptionsSnapshot
@section styles {
<abp-style src="/Pages/Pricing.css" />
@ -14,89 +12,82 @@
<main class="static-page container mt-5">
<h1 class="text-center mb-3">Pricing</h1>
<p class="text-center mb-5">Simple plans for everyone</p>
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-body px-4 py-4 text-center">
<h2 class="mt-4">Free Account</h2>
<p class="my-4">For individuals and small organizations.</p>
<div class="price mb-4">
<small>$</small>
<span>0</span>
<div class="row">
@foreach (var plan in Model.PlanInfos)
{
<div class="col-md-6">
<div class="card">
<div class="card-body px-4 py-4 text-center">
<h2 class="mt-4">@plan.PlanType Account</h2>
<p class="my-4">@L[plan.Description]</p>
<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.GetRestrictionInfoByCount(plan.Feature.MaxAllowedEventsCount) events in a year. </li>
<li><i class="fa fa-check-circle"></i> Up to @PlanInfoHelper.GetRestrictionInfoByCount(plan.Feature.MaxAllowedTracksCountInOneEvent).ToLower() tracks for an event. </li>
<li><i class="fa fa-check-circle"></i> Up to @PlanInfoHelper.GetRestrictionInfoByCount(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.IsPremium && plan.Price > 0)
{
<form id="Extend" method="post" asp-page-handler="ExtendToPremium">
<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.IsExtendable)
{
<form id="Upgrade" method="post" asp-page-handler="UpgradeToPremium">
<abp-input asp-for="TargetPlanToUpgrade" value="@plan.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 @plan.PlanType</a>
</form>
}
</div>
<ul class="list-check-icon mb-4 text-start">
<li><i class="fa fa-check-circle"></i> 2 events in a year. </li>
<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>
}
</div>
</div>
}
<h2 class="text-center mb-3">Frequently Asked Questions</h2>
<div id="accordion" class="mb-5 pb-5 faq-container">
<div class="faq" data-anchor-id="accordion12">
<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>
</div>
<div id="accordion12" class="collapse" data-bs-parent="#accordion" style="">
<div class="faq-body">
@{
var canExtendBeforeHowManyMonths = PremiumPlanInfoOptionsSnapshot.Value.OnePremiumPeriodAsMonth - PremiumPlanInfoOptionsSnapshot.Value.CanBeExtendedAfterHowManyMonths;
}
<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>
</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 Premium plan at any time.
</p>
</div>
</div>
</div>
</div>
<div id="accordion" class="mb-5 pb-5 faq-container">
<div class="faq" data-anchor-id="accordion12">
<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>
</div>
<div id="accordion12" class="collapse" data-bs-parent="#accordion" style="">
<div class="faq-body">
@foreach (var plan in Model.PlanInfos.Where(x => x.Price > 0))
{
@if (plan.IsExtendable)
{
var canExtendBeforeHowManyMonths = plan!.OnePremiumPeriodAsMonth - plan!.CanBeExtendedAfterHowManyMonths;
<p>For @plan.PlanType, @(canExtendBeforeHowManyMonths == plan!.OnePremiumPeriodAsMonth ? "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>
}
else
{
<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 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 Premium plan at any time.
</p>
</div>
</div>
</div>
</div>
</div>
</main>

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

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

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",
"ClientId": "EventHub_Web",
"ClientSecret": "1q2w3e*"
},
"PlanInfo": {
"Premium": {
"IsActive": true,
"Price": 29.90,
"IsExtendable": true,
"CanBeExtendedAfterHowManyMonths": 9,
"OnePremiumPeriodAsMonth": 12
}
}
}

Loading…
Cancel
Save