diff --git a/Api/Controllers/PaymentsController.cs b/Api/Controllers/PaymentsController.cs index 12ca50c..bfa63a7 100644 --- a/Api/Controllers/PaymentsController.cs +++ b/Api/Controllers/PaymentsController.cs @@ -21,6 +21,38 @@ public PaymentsController(ILogger logger, SubscriptionReposi StripeConfiguration.ApiKey = stripeSettings.ApiKey; } + [Authorize] + [HttpGet("manage")] + public async Task Manage() + { + var userId = GetUserId(); + var lastSubscription = await subscriptionRepository.GetLastSubscriptionByUserId(userId); + if (lastSubscription == null) + { + return BadRequest("User has no subscription"); + } + + string referrer = ""; + if (Request.Headers.ContainsKey("Referer")) + { + referrer = Request.Headers["Referer"].ToString(); + } + if (!referrer.StartsWith(frontendUrl)) { + referrer = frontendUrl; + } + + var service = new Stripe.BillingPortal.SessionService(); + var options = new Stripe.BillingPortal.SessionCreateOptions + { + Customer = lastSubscription.CustomerId, + ReturnUrl = referrer, + }; + var session = await service.CreateAsync(options); + var portalUrl = session.Url; + + return Ok(portalUrl); + } + [Authorize] [HttpGet("intent")] public async Task CreateCheckoutSession() @@ -112,7 +144,8 @@ await subscriptionRepository.UpsertSubscription( CustomerId = subscription.CustomerId, Status = subscription.Status.ToSubscriptionStatus(), ExpiresOn = subscription.CurrentPeriodEnd, - CreatedOn = stripeEvent.Created + CreatedOn = stripeEvent.Created, + CancelAtPeriodEnd = subscription.CancelAtPeriodEnd } ); return Ok(); diff --git a/Api/Controllers/UserController.cs b/Api/Controllers/UserController.cs index 3e14cfa..970ac3d 100644 --- a/Api/Controllers/UserController.cs +++ b/Api/Controllers/UserController.cs @@ -21,6 +21,13 @@ public async Task GetUserInfo() { var isPremium = await subscriptionRepository.IsUserPremium(GetUserId()); - return new UserInfo() { IsPremium = isPremium, ChatData = new ChatData() }; + DateTime? expireDate = await subscriptionRepository.GetExpireDate(GetUserId()); + bool? isAutoRenewActive = await subscriptionRepository.IsAutoRenewActive(GetUserId()); + return new UserInfo() { + IsPremium = isPremium, + ExpireDate = expireDate, + IsAutoRenewActive = isAutoRenewActive, + ChatData = new ChatData() + }; } } \ No newline at end of file diff --git a/Api/Dto/UserInfo.cs b/Api/Dto/UserInfo.cs index 0a34fe8..e41c77f 100644 --- a/Api/Dto/UserInfo.cs +++ b/Api/Dto/UserInfo.cs @@ -3,6 +3,8 @@ public class UserInfo { public bool IsPremium { get; set; } + public DateTime? ExpireDate { get; set; } + public bool? IsAutoRenewActive { get; set; } public ChatData ChatData { get; set; } = null!; } diff --git a/Application/Payments/SubscriptionRepository.cs b/Application/Payments/SubscriptionRepository.cs index 359d305..724e4c1 100644 --- a/Application/Payments/SubscriptionRepository.cs +++ b/Application/Payments/SubscriptionRepository.cs @@ -99,4 +99,24 @@ public async Task IsUserPremium(string userId) .FirstOrDefaultAsync(); //take the last added return result?.Status == SubscriptionStatus.Active; //and check if it is active } + + public async Task GetExpireDate(string userId) + { + var result = await context.Subscriptions //take the subscriptions + .Where(s => s.UserId == userId //of the user + && s.ExpiresOn > DateTime.UtcNow) //that are not expired + .OrderByDescending(s => s.CreatedOn) + .FirstOrDefaultAsync(); //take the last added + return result?.ExpiresOn; + } + + public async Task IsAutoRenewActive(string userId) + { + var result = await context.Subscriptions //take the subscriptions + .Where(s => s.UserId == userId //of the user + && s.ExpiresOn > DateTime.UtcNow) //that are not expired + .OrderByDescending(s => s.CreatedOn) + .FirstOrDefaultAsync(); //take the last added + return !(result?.CancelAtPeriodEnd ?? false); + } } \ No newline at end of file diff --git a/Domain/Subscription/Subscription.cs b/Domain/Subscription/Subscription.cs index 9543eab..7299248 100644 --- a/Domain/Subscription/Subscription.cs +++ b/Domain/Subscription/Subscription.cs @@ -14,4 +14,6 @@ public partial class Subscription public DateTime ExpiresOn { get; set; } public DateTime CreatedOn { get; set; } + + public bool CancelAtPeriodEnd { get; set; } } diff --git a/Infrastructure/AiPlugin/Migrations/AiPluginDbContextModelSnapshot.cs b/Infrastructure/AiPlugin/Migrations/AiPluginDbContextModelSnapshot.cs index 7771420..8368883 100644 --- a/Infrastructure/AiPlugin/Migrations/AiPluginDbContextModelSnapshot.cs +++ b/Infrastructure/AiPlugin/Migrations/AiPluginDbContextModelSnapshot.cs @@ -117,6 +117,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Id") .HasColumnType("nvarchar(450)"); + b.Property("CancelAtPeriodEnd") + .HasColumnType("bit"); + b.Property("CreatedOn") .HasColumnType("datetime2"); diff --git a/Infrastructure/Migrations/20231004175522_HandleSubCancelation.Designer.cs b/Infrastructure/Migrations/20231004175522_HandleSubCancelation.Designer.cs new file mode 100644 index 0000000..9947178 --- /dev/null +++ b/Infrastructure/Migrations/20231004175522_HandleSubCancelation.Designer.cs @@ -0,0 +1,164 @@ +// +using System; +using AiPlugin.Infrastructure; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AiPlugin.Migrations +{ + [DbContext(typeof(AiPluginDbContext))] + [Migration("20231004175522_HandleSubCancelation")] + partial class HandleSubCancelation + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("AiPlugin.Domain.Plugin.Plugin", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContactEmail") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreationDateTime") + .HasColumnType("datetime2"); + + b.Property("DescriptionForHuman") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("DescriptionForModel") + .IsRequired() + .HasMaxLength(8000) + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("LegalInfoUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LogoUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NameForHuman") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("NameForModel") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("isDeleted") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("Plugins"); + }); + + modelBuilder.Entity("AiPlugin.Domain.Plugin.Section", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(100000) + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("PluginId") + .HasColumnType("uniqueidentifier"); + + b.Property("isDeleted") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("PluginId"); + + b.ToTable("Sections"); + }); + + modelBuilder.Entity("Subscription", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("CancelAtPeriodEnd") + .HasColumnType("bit"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("CustomerId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExpiresOn") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Subscriptions"); + }); + + modelBuilder.Entity("AiPlugin.Domain.Plugin.Section", b => + { + b.HasOne("AiPlugin.Domain.Plugin.Plugin", null) + .WithMany("Sections") + .HasForeignKey("PluginId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AiPlugin.Domain.Plugin.Plugin", b => + { + b.Navigation("Sections"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Infrastructure/Migrations/20231004175522_HandleSubCancelation.cs b/Infrastructure/Migrations/20231004175522_HandleSubCancelation.cs new file mode 100644 index 0000000..7e7df28 --- /dev/null +++ b/Infrastructure/Migrations/20231004175522_HandleSubCancelation.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AiPlugin.Migrations +{ + /// + public partial class HandleSubCancelation : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "CancelAtPeriodEnd", + table: "Subscriptions", + type: "bit", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "CancelAtPeriodEnd", + table: "Subscriptions"); + } + } +}