WIP
This commit is contained in:
@@ -39,15 +39,15 @@ public static class ClaimsPrincipalExtensions
|
|||||||
return (string)claims.GetRequiredClaim<string>(ClaimTypes.Email);
|
return (string)claims.GetRequiredClaim<string>(ClaimTypes.Email);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static object? GetClaim<TValue>(this ClaimsPrincipal claims, string key)
|
private static object? GetClaim<TValue>(this ClaimsPrincipal claims, string key)
|
||||||
{
|
{
|
||||||
var claim = claims.FindFirst(key);
|
var claim = claims.FindFirst(key);
|
||||||
|
|
||||||
if (claim is null) return default;
|
if (claim is null) return default;
|
||||||
return claims.GetRequiredClaim<TValue>(key);
|
return claims.GetRequiredClaim<TValue>(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static object GetRequiredClaim<TValue>(this ClaimsPrincipal claims, string key)
|
private static object GetRequiredClaim<TValue>(this ClaimsPrincipal claims, string key)
|
||||||
{
|
{
|
||||||
var claim = claims.FindFirst(key);
|
var claim = claims.FindFirst(key);
|
||||||
|
|
||||||
|
|||||||
@@ -5,4 +5,5 @@ public class Creator
|
|||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public string StripeAccountId { get; set; }
|
public string StripeAccountId { get; set; }
|
||||||
|
public string PortraitUrl { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Memberships.Migrations
|
namespace Hutopy.Web.Features.Memberships.Data.Migrations
|
||||||
{
|
{
|
||||||
[DbContext(typeof(MembershipDbContext))]
|
[DbContext(typeof(MembershipDbContext))]
|
||||||
[Migration("20241011100852_Initial")]
|
[Migration("20241022191000_Initial")]
|
||||||
partial class Initial
|
partial class Initial
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -21,12 +21,12 @@ namespace Hutopy.Web.Features.Memberships.Migrations
|
|||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder
|
modelBuilder
|
||||||
.HasDefaultSchema("Membership")
|
.HasDefaultSchema("Membership")
|
||||||
.HasAnnotation("ProductVersion", "8.0.4")
|
.HasAnnotation("ProductVersion", "8.0.10")
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
modelBuilder.Entity("Hutopy.Web.Features.Membership.Data.Creator", b =>
|
modelBuilder.Entity("Hutopy.Web.Features.Memberships.Data.Creator", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
@@ -45,7 +45,7 @@ namespace Hutopy.Web.Features.Memberships.Migrations
|
|||||||
b.ToTable("Creators", "Membership");
|
b.ToTable("Creators", "Membership");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Hutopy.Web.Features.Membership.Data.Subscription", b =>
|
modelBuilder.Entity("Hutopy.Web.Features.Memberships.Data.Subscription", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
@@ -66,10 +66,12 @@ namespace Hutopy.Web.Features.Memberships.Migrations
|
|||||||
.HasColumnType("timestamp with time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
b.Property<string>("StripeSessionId")
|
b.Property<string>("StripeSessionId")
|
||||||
.HasColumnType("text");
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)");
|
||||||
|
|
||||||
b.Property<string>("StripeSubscriptionId")
|
b.Property<string>("StripeSubscriptionId")
|
||||||
.HasColumnType("text");
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)");
|
||||||
|
|
||||||
b.Property<Guid>("TierId")
|
b.Property<Guid>("TierId")
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
@@ -86,7 +88,7 @@ namespace Hutopy.Web.Features.Memberships.Migrations
|
|||||||
b.ToTable("Subscriptions", "Membership");
|
b.ToTable("Subscriptions", "Membership");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Hutopy.Web.Features.Membership.Data.Tier", b =>
|
modelBuilder.Entity("Hutopy.Web.Features.Memberships.Data.Tier", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
@@ -102,15 +104,32 @@ namespace Hutopy.Web.Features.Memberships.Migrations
|
|||||||
|
|
||||||
b.Property<string>("CurrencyCode")
|
b.Property<string>("CurrencyCode")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(4096)
|
||||||
|
.HasColumnType("character varying(4096)");
|
||||||
|
|
||||||
b.Property<string>("Name")
|
b.Property<string>("Name")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
b.Property<decimal>("Price")
|
b.Property<decimal>("Price")
|
||||||
.HasColumnType("numeric");
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<string>("StripePriceId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<string>("StripeProductId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("CreatorId");
|
b.HasIndex("CreatorId");
|
||||||
@@ -118,7 +137,7 @@ namespace Hutopy.Web.Features.Memberships.Migrations
|
|||||||
b.ToTable("Tiers", "Membership");
|
b.ToTable("Tiers", "Membership");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Hutopy.Web.Features.Membership.Data.Tip", b =>
|
modelBuilder.Entity("Hutopy.Web.Features.Memberships.Data.Tip", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
@@ -158,12 +177,17 @@ namespace Hutopy.Web.Features.Memberships.Migrations
|
|||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<Guid>("TransactionId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TransactionId");
|
||||||
|
|
||||||
b.ToTable("Tips", "Membership");
|
b.ToTable("Tips", "Membership");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Hutopy.Web.Features.Membership.Data.Transaction", b =>
|
modelBuilder.Entity("Hutopy.Web.Features.Memberships.Data.Transaction", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
@@ -177,10 +201,16 @@ namespace Hutopy.Web.Features.Memberships.Migrations
|
|||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("timestamp with time zone")
|
||||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
|
||||||
b.Property<string>("StripeCheckoutSessionId")
|
b.Property<string>("Currency")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("StripeInvoiceUrl")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<Guid?>("SubscriptionId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
b.Property<DateTime>("Timestamp")
|
b.Property<DateTime>("Timestamp")
|
||||||
.HasColumnType("timestamp with time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
@@ -190,18 +220,20 @@ namespace Hutopy.Web.Features.Memberships.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("SubscriptionId");
|
||||||
|
|
||||||
b.ToTable("Transactions", "Membership");
|
b.ToTable("Transactions", "Membership");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Hutopy.Web.Features.Membership.Data.Subscription", b =>
|
modelBuilder.Entity("Hutopy.Web.Features.Memberships.Data.Subscription", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Hutopy.Web.Features.Membership.Data.Creator", "Creator")
|
b.HasOne("Hutopy.Web.Features.Memberships.Data.Creator", "Creator")
|
||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey("CreatorId")
|
.HasForeignKey("CreatorId")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
b.HasOne("Hutopy.Web.Features.Membership.Data.Tier", "Tier")
|
b.HasOne("Hutopy.Web.Features.Memberships.Data.Tier", "Tier")
|
||||||
.WithMany("Subscriptions")
|
.WithMany("Subscriptions")
|
||||||
.HasForeignKey("TierId")
|
.HasForeignKey("TierId")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
@@ -212,9 +244,9 @@ namespace Hutopy.Web.Features.Memberships.Migrations
|
|||||||
b.Navigation("Tier");
|
b.Navigation("Tier");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Hutopy.Web.Features.Membership.Data.Tier", b =>
|
modelBuilder.Entity("Hutopy.Web.Features.Memberships.Data.Tier", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Hutopy.Web.Features.Membership.Data.Creator", "Creator")
|
b.HasOne("Hutopy.Web.Features.Memberships.Data.Creator", "Creator")
|
||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey("CreatorId")
|
.HasForeignKey("CreatorId")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
@@ -223,7 +255,30 @@ namespace Hutopy.Web.Features.Memberships.Migrations
|
|||||||
b.Navigation("Creator");
|
b.Navigation("Creator");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Hutopy.Web.Features.Membership.Data.Tier", b =>
|
modelBuilder.Entity("Hutopy.Web.Features.Memberships.Data.Tip", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Hutopy.Web.Features.Memberships.Data.Transaction", "Transaction")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TransactionId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Transaction");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Hutopy.Web.Features.Memberships.Data.Transaction", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Hutopy.Web.Features.Memberships.Data.Subscription", null)
|
||||||
|
.WithMany("Transactions")
|
||||||
|
.HasForeignKey("SubscriptionId");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Hutopy.Web.Features.Memberships.Data.Subscription", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Transactions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Hutopy.Web.Features.Memberships.Data.Tier", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("Subscriptions");
|
b.Navigation("Subscriptions");
|
||||||
});
|
});
|
||||||
@@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
|||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Memberships.Migrations
|
namespace Hutopy.Web.Features.Memberships.Data.Migrations
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public partial class Initial : Migration
|
public partial class Initial : Migration
|
||||||
@@ -28,44 +28,6 @@ namespace Hutopy.Web.Features.Memberships.Migrations
|
|||||||
table.PrimaryKey("PK_Creators", x => x.Id);
|
table.PrimaryKey("PK_Creators", x => x.Id);
|
||||||
});
|
});
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Tips",
|
|
||||||
schema: "Membership",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
|
||||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"),
|
|
||||||
StripeSessionId = table.Column<string>(type: "text", nullable: false),
|
|
||||||
TipperId = table.Column<Guid>(type: "uuid", nullable: false),
|
|
||||||
TipperName = table.Column<string>(type: "text", nullable: false),
|
|
||||||
CreatorId = table.Column<Guid>(type: "uuid", nullable: false),
|
|
||||||
CreatorName = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Amount = table.Column<decimal>(type: "numeric", nullable: false),
|
|
||||||
Currency = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Message = table.Column<string>(type: "text", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Tips", x => x.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Transactions",
|
|
||||||
schema: "Membership",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
|
||||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"),
|
|
||||||
StripeCheckoutSessionId = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Amount = table.Column<decimal>(type: "numeric", nullable: false),
|
|
||||||
Type = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Timestamp = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Transactions", x => x.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
migrationBuilder.CreateTable(
|
||||||
name: "Tiers",
|
name: "Tiers",
|
||||||
schema: "Membership",
|
schema: "Membership",
|
||||||
@@ -74,9 +36,12 @@ namespace Hutopy.Web.Features.Memberships.Migrations
|
|||||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"),
|
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"),
|
||||||
CreatorId = table.Column<Guid>(type: "uuid", nullable: false),
|
CreatorId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
Name = table.Column<string>(type: "text", nullable: false),
|
Name = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||||
|
Description = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: false),
|
||||||
Price = table.Column<decimal>(type: "numeric", nullable: false),
|
Price = table.Column<decimal>(type: "numeric", nullable: false),
|
||||||
CurrencyCode = table.Column<string>(type: "text", nullable: false)
|
CurrencyCode = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||||
|
StripeProductId = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||||
|
StripePriceId = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false)
|
||||||
},
|
},
|
||||||
constraints: table =>
|
constraints: table =>
|
||||||
{
|
{
|
||||||
@@ -102,8 +67,8 @@ namespace Hutopy.Web.Features.Memberships.Migrations
|
|||||||
TierId = table.Column<Guid>(type: "uuid", nullable: false),
|
TierId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
StartDate = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
StartDate = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||||
EndDate = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
EndDate = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||||
StripeSessionId = table.Column<string>(type: "text", nullable: true),
|
StripeSessionId = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true),
|
||||||
StripeSubscriptionId = table.Column<string>(type: "text", nullable: true)
|
StripeSubscriptionId = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true)
|
||||||
},
|
},
|
||||||
constraints: table =>
|
constraints: table =>
|
||||||
{
|
{
|
||||||
@@ -124,6 +89,60 @@ namespace Hutopy.Web.Features.Memberships.Migrations
|
|||||||
onDelete: ReferentialAction.Cascade);
|
onDelete: ReferentialAction.Cascade);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Transactions",
|
||||||
|
schema: "Membership",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"),
|
||||||
|
Amount = table.Column<decimal>(type: "numeric", nullable: false),
|
||||||
|
Currency = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Type = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Timestamp = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
StripeInvoiceUrl = table.Column<string>(type: "text", nullable: true),
|
||||||
|
SubscriptionId = table.Column<Guid>(type: "uuid", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Transactions", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Transactions_Subscriptions_SubscriptionId",
|
||||||
|
column: x => x.SubscriptionId,
|
||||||
|
principalSchema: "Membership",
|
||||||
|
principalTable: "Subscriptions",
|
||||||
|
principalColumn: "Id");
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Tips",
|
||||||
|
schema: "Membership",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"),
|
||||||
|
StripeSessionId = table.Column<string>(type: "text", nullable: false),
|
||||||
|
TipperId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
TipperName = table.Column<string>(type: "text", nullable: false),
|
||||||
|
CreatorId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
CreatorName = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Amount = table.Column<decimal>(type: "numeric", nullable: false),
|
||||||
|
Currency = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Message = table.Column<string>(type: "text", nullable: false),
|
||||||
|
TransactionId = table.Column<Guid>(type: "uuid", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Tips", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Tips_Transactions_TransactionId",
|
||||||
|
column: x => x.TransactionId,
|
||||||
|
principalSchema: "Membership",
|
||||||
|
principalTable: "Transactions",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
migrationBuilder.CreateIndex(
|
||||||
name: "IX_Subscriptions_CreatorId",
|
name: "IX_Subscriptions_CreatorId",
|
||||||
schema: "Membership",
|
schema: "Membership",
|
||||||
@@ -141,15 +160,23 @@ namespace Hutopy.Web.Features.Memberships.Migrations
|
|||||||
schema: "Membership",
|
schema: "Membership",
|
||||||
table: "Tiers",
|
table: "Tiers",
|
||||||
column: "CreatorId");
|
column: "CreatorId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Tips_TransactionId",
|
||||||
|
schema: "Membership",
|
||||||
|
table: "Tips",
|
||||||
|
column: "TransactionId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Transactions_SubscriptionId",
|
||||||
|
schema: "Membership",
|
||||||
|
table: "Transactions",
|
||||||
|
column: "SubscriptionId");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Subscriptions",
|
|
||||||
schema: "Membership");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
name: "Tips",
|
name: "Tips",
|
||||||
schema: "Membership");
|
schema: "Membership");
|
||||||
@@ -158,6 +185,10 @@ namespace Hutopy.Web.Features.Memberships.Migrations
|
|||||||
name: "Transactions",
|
name: "Transactions",
|
||||||
schema: "Membership");
|
schema: "Membership");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Subscriptions",
|
||||||
|
schema: "Membership");
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
name: "Tiers",
|
name: "Tiers",
|
||||||
schema: "Membership");
|
schema: "Membership");
|
||||||
292
src/Web/Features/Memberships/Data/Migrations/20241022203207_PortraitUrlToCreator.Designer.cs
generated
Normal file
292
src/Web/Features/Memberships/Data/Migrations/20241022203207_PortraitUrlToCreator.Designer.cs
generated
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Hutopy.Web.Features.Memberships.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Hutopy.Web.Features.Memberships.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(MembershipDbContext))]
|
||||||
|
[Migration("20241022203207_PortraitUrlToCreator")]
|
||||||
|
partial class PortraitUrlToCreator
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasDefaultSchema("Membership")
|
||||||
|
.HasAnnotation("ProductVersion", "8.0.10")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Hutopy.Web.Features.Memberships.Data.Creator", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("PortraitUrl")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("StripeAccountId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Creators", "Membership");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Hutopy.Web.Features.Memberships.Data.Subscription", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
|
||||||
|
b.Property<Guid>("CreatorId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("EndDate")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("StartDate")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("StripeSessionId")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)");
|
||||||
|
|
||||||
|
b.Property<string>("StripeSubscriptionId")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)");
|
||||||
|
|
||||||
|
b.Property<Guid>("TierId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CreatorId");
|
||||||
|
|
||||||
|
b.HasIndex("TierId");
|
||||||
|
|
||||||
|
b.ToTable("Subscriptions", "Membership");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Hutopy.Web.Features.Memberships.Data.Tier", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
|
||||||
|
b.Property<Guid>("CreatorId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("CurrencyCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(4096)
|
||||||
|
.HasColumnType("character varying(4096)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<decimal>("Price")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<string>("StripePriceId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<string>("StripeProductId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CreatorId");
|
||||||
|
|
||||||
|
b.ToTable("Tiers", "Membership");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Hutopy.Web.Features.Memberships.Data.Tip", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<decimal>("Amount")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
|
||||||
|
b.Property<Guid>("CreatorId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("CreatorName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Currency")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Message")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("StripeSessionId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<Guid>("TipperId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("TipperName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<Guid>("TransactionId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TransactionId");
|
||||||
|
|
||||||
|
b.ToTable("Tips", "Membership");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Hutopy.Web.Features.Memberships.Data.Transaction", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<decimal>("Amount")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
|
||||||
|
b.Property<string>("Currency")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("StripeInvoiceUrl")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<Guid?>("SubscriptionId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Timestamp")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("SubscriptionId");
|
||||||
|
|
||||||
|
b.ToTable("Transactions", "Membership");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Hutopy.Web.Features.Memberships.Data.Subscription", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Hutopy.Web.Features.Memberships.Data.Creator", "Creator")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("CreatorId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Hutopy.Web.Features.Memberships.Data.Tier", "Tier")
|
||||||
|
.WithMany("Subscriptions")
|
||||||
|
.HasForeignKey("TierId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Creator");
|
||||||
|
|
||||||
|
b.Navigation("Tier");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Hutopy.Web.Features.Memberships.Data.Tier", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Hutopy.Web.Features.Memberships.Data.Creator", "Creator")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("CreatorId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Creator");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Hutopy.Web.Features.Memberships.Data.Tip", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Hutopy.Web.Features.Memberships.Data.Transaction", "Transaction")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TransactionId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Transaction");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Hutopy.Web.Features.Memberships.Data.Transaction", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Hutopy.Web.Features.Memberships.Data.Subscription", null)
|
||||||
|
.WithMany("Transactions")
|
||||||
|
.HasForeignKey("SubscriptionId");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Hutopy.Web.Features.Memberships.Data.Subscription", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Transactions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Hutopy.Web.Features.Memberships.Data.Tier", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Subscriptions");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Hutopy.Web.Features.Memberships.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class PortraitUrlToCreator : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "PortraitUrl",
|
||||||
|
schema: "Membership",
|
||||||
|
table: "Creators",
|
||||||
|
type: "text",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "PortraitUrl",
|
||||||
|
schema: "Membership",
|
||||||
|
table: "Creators");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Memberships.Migrations
|
namespace Hutopy.Web.Features.Memberships.Data.Migrations
|
||||||
{
|
{
|
||||||
[DbContext(typeof(MembershipDbContext))]
|
[DbContext(typeof(MembershipDbContext))]
|
||||||
partial class MembershipDbContextModelSnapshot : ModelSnapshot
|
partial class MembershipDbContextModelSnapshot : ModelSnapshot
|
||||||
@@ -18,12 +18,12 @@ namespace Hutopy.Web.Features.Memberships.Migrations
|
|||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder
|
modelBuilder
|
||||||
.HasDefaultSchema("Membership")
|
.HasDefaultSchema("Membership")
|
||||||
.HasAnnotation("ProductVersion", "8.0.4")
|
.HasAnnotation("ProductVersion", "8.0.10")
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
modelBuilder.Entity("Hutopy.Web.Features.Membership.Data.Creator", b =>
|
modelBuilder.Entity("Hutopy.Web.Features.Memberships.Data.Creator", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
@@ -33,6 +33,10 @@ namespace Hutopy.Web.Features.Memberships.Migrations
|
|||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("PortraitUrl")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
b.Property<string>("StripeAccountId")
|
b.Property<string>("StripeAccountId")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
@@ -42,7 +46,7 @@ namespace Hutopy.Web.Features.Memberships.Migrations
|
|||||||
b.ToTable("Creators", "Membership");
|
b.ToTable("Creators", "Membership");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Hutopy.Web.Features.Membership.Data.Subscription", b =>
|
modelBuilder.Entity("Hutopy.Web.Features.Memberships.Data.Subscription", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
@@ -63,10 +67,12 @@ namespace Hutopy.Web.Features.Memberships.Migrations
|
|||||||
.HasColumnType("timestamp with time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
b.Property<string>("StripeSessionId")
|
b.Property<string>("StripeSessionId")
|
||||||
.HasColumnType("text");
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)");
|
||||||
|
|
||||||
b.Property<string>("StripeSubscriptionId")
|
b.Property<string>("StripeSubscriptionId")
|
||||||
.HasColumnType("text");
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)");
|
||||||
|
|
||||||
b.Property<Guid>("TierId")
|
b.Property<Guid>("TierId")
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
@@ -83,7 +89,7 @@ namespace Hutopy.Web.Features.Memberships.Migrations
|
|||||||
b.ToTable("Subscriptions", "Membership");
|
b.ToTable("Subscriptions", "Membership");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Hutopy.Web.Features.Membership.Data.Tier", b =>
|
modelBuilder.Entity("Hutopy.Web.Features.Memberships.Data.Tier", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
@@ -99,15 +105,32 @@ namespace Hutopy.Web.Features.Memberships.Migrations
|
|||||||
|
|
||||||
b.Property<string>("CurrencyCode")
|
b.Property<string>("CurrencyCode")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(4096)
|
||||||
|
.HasColumnType("character varying(4096)");
|
||||||
|
|
||||||
b.Property<string>("Name")
|
b.Property<string>("Name")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
b.Property<decimal>("Price")
|
b.Property<decimal>("Price")
|
||||||
.HasColumnType("numeric");
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<string>("StripePriceId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<string>("StripeProductId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("CreatorId");
|
b.HasIndex("CreatorId");
|
||||||
@@ -115,7 +138,7 @@ namespace Hutopy.Web.Features.Memberships.Migrations
|
|||||||
b.ToTable("Tiers", "Membership");
|
b.ToTable("Tiers", "Membership");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Hutopy.Web.Features.Membership.Data.Tip", b =>
|
modelBuilder.Entity("Hutopy.Web.Features.Memberships.Data.Tip", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
@@ -155,12 +178,17 @@ namespace Hutopy.Web.Features.Memberships.Migrations
|
|||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<Guid>("TransactionId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TransactionId");
|
||||||
|
|
||||||
b.ToTable("Tips", "Membership");
|
b.ToTable("Tips", "Membership");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Hutopy.Web.Features.Membership.Data.Transaction", b =>
|
modelBuilder.Entity("Hutopy.Web.Features.Memberships.Data.Transaction", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
@@ -174,10 +202,16 @@ namespace Hutopy.Web.Features.Memberships.Migrations
|
|||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("timestamp with time zone")
|
||||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
|
||||||
b.Property<string>("StripeCheckoutSessionId")
|
b.Property<string>("Currency")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("StripeInvoiceUrl")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<Guid?>("SubscriptionId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
b.Property<DateTime>("Timestamp")
|
b.Property<DateTime>("Timestamp")
|
||||||
.HasColumnType("timestamp with time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
@@ -187,18 +221,20 @@ namespace Hutopy.Web.Features.Memberships.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("SubscriptionId");
|
||||||
|
|
||||||
b.ToTable("Transactions", "Membership");
|
b.ToTable("Transactions", "Membership");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Hutopy.Web.Features.Membership.Data.Subscription", b =>
|
modelBuilder.Entity("Hutopy.Web.Features.Memberships.Data.Subscription", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Hutopy.Web.Features.Membership.Data.Creator", "Creator")
|
b.HasOne("Hutopy.Web.Features.Memberships.Data.Creator", "Creator")
|
||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey("CreatorId")
|
.HasForeignKey("CreatorId")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
b.HasOne("Hutopy.Web.Features.Membership.Data.Tier", "Tier")
|
b.HasOne("Hutopy.Web.Features.Memberships.Data.Tier", "Tier")
|
||||||
.WithMany("Subscriptions")
|
.WithMany("Subscriptions")
|
||||||
.HasForeignKey("TierId")
|
.HasForeignKey("TierId")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
@@ -209,9 +245,9 @@ namespace Hutopy.Web.Features.Memberships.Migrations
|
|||||||
b.Navigation("Tier");
|
b.Navigation("Tier");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Hutopy.Web.Features.Membership.Data.Tier", b =>
|
modelBuilder.Entity("Hutopy.Web.Features.Memberships.Data.Tier", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Hutopy.Web.Features.Membership.Data.Creator", "Creator")
|
b.HasOne("Hutopy.Web.Features.Memberships.Data.Creator", "Creator")
|
||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey("CreatorId")
|
.HasForeignKey("CreatorId")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
@@ -220,7 +256,30 @@ namespace Hutopy.Web.Features.Memberships.Migrations
|
|||||||
b.Navigation("Creator");
|
b.Navigation("Creator");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Hutopy.Web.Features.Membership.Data.Tier", b =>
|
modelBuilder.Entity("Hutopy.Web.Features.Memberships.Data.Tip", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Hutopy.Web.Features.Memberships.Data.Transaction", "Transaction")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TransactionId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Transaction");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Hutopy.Web.Features.Memberships.Data.Transaction", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Hutopy.Web.Features.Memberships.Data.Subscription", null)
|
||||||
|
.WithMany("Transactions")
|
||||||
|
.HasForeignKey("SubscriptionId");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Hutopy.Web.Features.Memberships.Data.Subscription", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Transactions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Hutopy.Web.Features.Memberships.Data.Tier", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("Subscriptions");
|
b.Navigation("Subscriptions");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Memberships.Data;
|
namespace Hutopy.Web.Features.Memberships.Data;
|
||||||
|
|
||||||
public class Subscription
|
public class Subscription
|
||||||
@@ -11,8 +13,9 @@ public class Subscription
|
|||||||
public Tier Tier { get; set; }
|
public Tier Tier { get; set; }
|
||||||
public DateTimeOffset StartDate { get; set; }
|
public DateTimeOffset StartDate { get; set; }
|
||||||
public DateTimeOffset? EndDate { get; set; }
|
public DateTimeOffset? EndDate { get; set; }
|
||||||
public bool IsActive => EndDate == null || EndDate > DateTime.UtcNow;
|
public bool IsActive => EndDate == null || EndDate > DateTimeOffset.UtcNow;
|
||||||
public string? StripeSessionId { get; set; }
|
[MaxLength(255)]public string? StripeSessionId { get; set; }
|
||||||
public string? StripeSubscriptionId { get; set; }
|
[MaxLength(255)]public string? StripeSubscriptionId { get; set; }
|
||||||
|
|
||||||
|
public ICollection<Transaction> Transactions { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Memberships.Data;
|
namespace Hutopy.Web.Features.Memberships.Data;
|
||||||
|
|
||||||
public class Tier
|
public class Tier
|
||||||
@@ -5,10 +7,13 @@ public class Tier
|
|||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
public DateTime CreatedAt { get; set; }
|
public DateTime CreatedAt { get; set; }
|
||||||
public Guid CreatorId { get; set; }
|
public Guid CreatorId { get; set; }
|
||||||
public Creator Creator { get; set; }
|
public Creator Creator { get; set; } = null!;
|
||||||
public string Name { get; set; }
|
[MaxLength(128)] public string Name { get; set; } = null!;
|
||||||
|
[MaxLength(4096)] public string Description { get; set; } = null!;
|
||||||
public decimal Price { get; set; }
|
public decimal Price { get; set; }
|
||||||
public string CurrencyCode { get; set; }
|
[MaxLength(128)] public string CurrencyCode { get; set; } = null!;
|
||||||
|
[MaxLength(128)] public string StripeProductId { get; set; } = null!;
|
||||||
public ICollection<Subscription> Subscriptions { get; set; }
|
[MaxLength(128)] public string StripePriceId { get; set; } = null!;
|
||||||
|
|
||||||
|
public ICollection<Subscription> Subscriptions { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,4 +12,7 @@ public class Tip
|
|||||||
public decimal Amount { get; set; }
|
public decimal Amount { get; set; }
|
||||||
public string Currency { get; set; }
|
public string Currency { get; set; }
|
||||||
public string Message { get; set; }
|
public string Message { get; set; }
|
||||||
|
|
||||||
|
public Guid TransactionId { get; set; }
|
||||||
|
public Transaction Transaction { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ public class Transaction
|
|||||||
{
|
{
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
public DateTimeOffset CreatedAt { get; set; }
|
public DateTimeOffset CreatedAt { get; set; }
|
||||||
public string StripeCheckoutSessionId { get; set; }
|
|
||||||
public decimal Amount { get; set; }
|
public decimal Amount { get; set; }
|
||||||
|
public string Currency { get; set; }
|
||||||
public string Type { get; set; } // Subscription, Tip
|
public string Type { get; set; } // Subscription, Tip
|
||||||
public DateTime Timestamp { get; set; }
|
public DateTime Timestamp { get; set; }
|
||||||
|
public string? StripeInvoiceUrl { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using Hutopy.Web.Features.Memberships.Data;
|
using Hutopy.Web.Features.Memberships.Data;
|
||||||
using Hutopy.Web.Features.Memberships.Services;
|
using Hutopy.Web.Features.Memberships.Infrastructure;
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Memberships;
|
namespace Hutopy.Web.Features.Memberships;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using Hutopy.Web.Features.Memberships.Data;
|
using Hutopy.Web.Features.Memberships.Data;
|
||||||
using Hutopy.Web.Features.Memberships.Services;
|
using Hutopy.Web.Features.Memberships.Infrastructure;
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Memberships.Handlers;
|
namespace Hutopy.Web.Features.Memberships.Handlers;
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ public class CancelSubscriptionRequest
|
|||||||
}
|
}
|
||||||
|
|
||||||
public class CancelSubscriptionHandler(
|
public class CancelSubscriptionHandler(
|
||||||
MembershipDbContext dbDbContext,
|
MembershipDbContext dbContext,
|
||||||
StripeService stripeService)
|
StripeService stripeService)
|
||||||
: Endpoint<CancelSubscriptionRequest>
|
: Endpoint<CancelSubscriptionRequest>
|
||||||
{
|
{
|
||||||
@@ -24,7 +24,7 @@ public class CancelSubscriptionHandler(
|
|||||||
CancelSubscriptionRequest req,
|
CancelSubscriptionRequest req,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
var subscription = await dbDbContext
|
var subscription = await dbContext
|
||||||
.Subscriptions
|
.Subscriptions
|
||||||
.FindAsync(
|
.FindAsync(
|
||||||
[req.SubscriptionId],
|
[req.SubscriptionId],
|
||||||
@@ -41,7 +41,7 @@ public class CancelSubscriptionHandler(
|
|||||||
|
|
||||||
// Update subscription in the system
|
// Update subscription in the system
|
||||||
subscription.EndDate = DateTime.UtcNow;
|
subscription.EndDate = DateTime.UtcNow;
|
||||||
await dbDbContext.SaveChangesAsync(ct);
|
await dbContext.SaveChangesAsync(ct);
|
||||||
|
|
||||||
await SendOkAsync(subscription.Id, ct);
|
await SendOkAsync(subscription.Id, ct);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using Hutopy.Web.Common.Security;
|
using Hutopy.Web.Common.Security;
|
||||||
using Hutopy.Web.Features.Memberships.Data;
|
using Hutopy.Web.Features.Memberships.Data;
|
||||||
using Hutopy.Web.Features.Memberships.Services;
|
using Hutopy.Web.Features.Memberships.Infrastructure;
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Memberships.Handlers;
|
namespace Hutopy.Web.Features.Memberships.Handlers;
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
using Hutopy.Web.Features.Memberships.Data;
|
using Hutopy.Web.Features.Memberships.Data;
|
||||||
|
using Hutopy.Web.Features.Memberships.Infrastructure;
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Memberships.Handlers;
|
namespace Hutopy.Web.Features.Memberships.Handlers;
|
||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public class CreateMembershipTierRequest
|
public record struct CreateMembershipTierRequest(
|
||||||
{
|
Guid CreatorId,
|
||||||
public Guid CreatorId { get; set; }
|
string Name,
|
||||||
public string Name { get; set; }
|
string Description,
|
||||||
public decimal Price { get; set; }
|
decimal Price,
|
||||||
}
|
string Currency = "CAD");
|
||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public class CreateMembershipTierEndpoint(
|
public class CreateMembershipTierEndpoint(
|
||||||
MembershipDbContext dbDbContext)
|
MembershipDbContext dbContext,
|
||||||
|
StripeService stripe)
|
||||||
: Endpoint<CreateMembershipTierRequest>
|
: Endpoint<CreateMembershipTierRequest>
|
||||||
{
|
{
|
||||||
public override void Configure()
|
public override void Configure()
|
||||||
@@ -25,11 +27,29 @@ public class CreateMembershipTierEndpoint(
|
|||||||
CreateMembershipTierRequest req,
|
CreateMembershipTierRequest req,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
var tier = dbDbContext
|
var tierId = Guid.NewGuid();
|
||||||
.Tiers
|
|
||||||
.Add(new Tier { CreatorId = req.CreatorId, Price = req.Price, Name = req.Name });
|
|
||||||
|
|
||||||
await dbDbContext.SaveChangesAsync(ct);
|
var productId = await stripe.CreateProductAsync(
|
||||||
|
req.CreatorId,
|
||||||
|
tierId,
|
||||||
|
req.Name,
|
||||||
|
req.Currency,
|
||||||
|
req.Price);
|
||||||
|
|
||||||
|
// Record the new Tier
|
||||||
|
var tier = new Tier
|
||||||
|
{
|
||||||
|
Id = tierId,
|
||||||
|
CreatorId = req.CreatorId,
|
||||||
|
Price = req.Price,
|
||||||
|
Name = req.Name,
|
||||||
|
Description = req.Description,
|
||||||
|
StripeProductId = productId,
|
||||||
|
};
|
||||||
|
|
||||||
|
dbContext.Tiers.Add(tier);
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync(ct);
|
||||||
|
|
||||||
await SendOkAsync(tier, ct);
|
await SendOkAsync(tier, ct);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,21 @@
|
|||||||
using Hutopy.Web.Common;
|
using Hutopy.Web.Common.Security;
|
||||||
using Hutopy.Web.Common.Security;
|
|
||||||
using Hutopy.Web.Features.Memberships.Data;
|
using Hutopy.Web.Features.Memberships.Data;
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Memberships.Handlers;
|
namespace Hutopy.Web.Features.Memberships.Handlers;
|
||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public class GetActiveSubscriptionsRequest;
|
public record struct GetActiveSubscriptionsResponse(
|
||||||
|
Guid Id,
|
||||||
|
Guid CreatorId,
|
||||||
|
string CreatorName,
|
||||||
|
string CreatorPortraitUrl,
|
||||||
|
DateTimeOffset StartDate,
|
||||||
|
DateTimeOffset? EndDate);
|
||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public class GetActiveSubscriptionsHandler(
|
public class GetActiveSubscriptionsHandler(
|
||||||
MembershipDbContext dbDbContext)
|
MembershipDbContext dbContext)
|
||||||
: Endpoint<GetActiveSubscriptionsRequest>
|
: EndpointWithoutRequest<List<GetActiveSubscriptionsResponse>>
|
||||||
{
|
{
|
||||||
public override void Configure()
|
public override void Configure()
|
||||||
{
|
{
|
||||||
@@ -19,13 +24,19 @@ public class GetActiveSubscriptionsHandler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
public override async Task HandleAsync(
|
public override async Task HandleAsync(
|
||||||
GetActiveSubscriptionsRequest req,
|
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
var subscriptions = await dbDbContext
|
var subscriptions = await dbContext
|
||||||
.Subscriptions
|
.Subscriptions
|
||||||
.Where(subscription => subscription.UserId == User.GetUserId())
|
.Where(subscription => subscription.UserId == User.GetUserId())
|
||||||
.Where(subscription => subscription.IsActive)
|
.Where(subscription => subscription.EndDate == null || subscription.EndDate > DateTimeOffset.UtcNow)
|
||||||
|
.Select(subscription => new GetActiveSubscriptionsResponse(
|
||||||
|
subscription.Id,
|
||||||
|
subscription.Creator.Id,
|
||||||
|
subscription.Creator.Name,
|
||||||
|
subscription.Creator.PortraitUrl,
|
||||||
|
subscription.StartDate,
|
||||||
|
subscription.EndDate))
|
||||||
.ToListAsync(ct);
|
.ToListAsync(ct);
|
||||||
|
|
||||||
await SendOkAsync(subscriptions, ct);
|
await SendOkAsync(subscriptions, ct);
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
namespace Hutopy.Web.Features.Memberships.Handlers;
|
namespace Hutopy.Web.Features.Memberships.Handlers;
|
||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public class GetMembershipTierRequest
|
public record GetMembershipTierRequest
|
||||||
{
|
{
|
||||||
public Guid CreatorId { get; set; }
|
public Guid CreatorId { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -13,35 +13,40 @@ public record struct TierModel(
|
|||||||
Guid Id,
|
Guid Id,
|
||||||
DateTime CreatedAt,
|
DateTime CreatedAt,
|
||||||
string Name,
|
string Name,
|
||||||
|
string Description,
|
||||||
decimal Price,
|
decimal Price,
|
||||||
string CurrencyCode);
|
string CurrencyCode,
|
||||||
|
string StripeProductId);
|
||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public class GetMembershipTierEndpoint(
|
public class GetMembershipTierEndpoint(
|
||||||
MembershipDbContext dbDbContext)
|
MembershipDbContext dbContext)
|
||||||
: Endpoint<CreateMembershipTierRequest, List<TierModel>>
|
: Endpoint<GetMembershipTierRequest, List<TierModel>>
|
||||||
{
|
{
|
||||||
public override void Configure()
|
public override void Configure()
|
||||||
{
|
{
|
||||||
Get("/api/membership/tiers");
|
Get("/api/membership/tiers/{CreatorId:guid}");
|
||||||
Options(o => o.WithTags("Memberships"));
|
Options(o => o.WithTags("Memberships"));
|
||||||
|
AllowAnonymous();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task HandleAsync(
|
public override async Task HandleAsync(
|
||||||
CreateMembershipTierRequest req,
|
GetMembershipTierRequest req,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
var tiers = await dbDbContext
|
var tiers = await dbContext
|
||||||
.Tiers
|
.Tiers
|
||||||
.Where(tier => tier.CreatorId == req.CreatorId)
|
.Where(tier => tier.CreatorId == req.CreatorId)
|
||||||
.Select(tier => new TierModel(
|
.Select(tier => new TierModel(
|
||||||
tier.Id,
|
tier.Id,
|
||||||
tier.CreatedAt,
|
tier.CreatedAt,
|
||||||
tier.Name,
|
tier.Name,
|
||||||
|
tier.Description,
|
||||||
tier.Price,
|
tier.Price,
|
||||||
tier.CurrencyCode))
|
tier.CurrencyCode,
|
||||||
|
tier.StripeProductId))
|
||||||
.ToListAsync(ct);
|
.ToListAsync(ct);
|
||||||
|
|
||||||
await SendOkAsync(tiers, ct);
|
await SendOkAsync(tiers, ct);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ public record struct TipReceivedModel(
|
|||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public class GetReceivedTipsHandler(
|
public class GetReceivedTipsHandler(
|
||||||
MembershipDbContext dbDbContext)
|
MembershipDbContext dbContext)
|
||||||
: EndpointWithoutRequest<List<TipReceivedModel>>
|
: EndpointWithoutRequest<List<TipReceivedModel>>
|
||||||
{
|
{
|
||||||
public override void Configure()
|
public override void Configure()
|
||||||
@@ -28,7 +28,7 @@ public class GetReceivedTipsHandler(
|
|||||||
public override async Task HandleAsync(
|
public override async Task HandleAsync(
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
var tipsReceived = await dbDbContext
|
var tipsReceived = await dbContext
|
||||||
.Tips
|
.Tips
|
||||||
.Where(tip => tip.CreatorId == User.GetUserId())
|
.Where(tip => tip.CreatorId == User.GetUserId())
|
||||||
.Select(tip => new TipReceivedModel(
|
.Select(tip => new TipReceivedModel(
|
||||||
|
|||||||
@@ -1,21 +1,10 @@
|
|||||||
using Hutopy.Web.Features.Memberships.Data;
|
using Hutopy.Web.Features.Memberships.Infrastructure;
|
||||||
using Hutopy.Web.Features.Memberships.Services;
|
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Stripe;
|
using Stripe;
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Memberships.Handlers;
|
namespace Hutopy.Web.Features.Memberships.Handlers;
|
||||||
|
|
||||||
public static class StripeEvents
|
|
||||||
{
|
|
||||||
public const string SubscriptionCreated = "subscription_created";
|
|
||||||
public const string CustomerSubscriptionDeleted = "customer.subscription_deleted";
|
|
||||||
public const string InvoicePaymentSucceeded = "invoice.payment_succeeded";
|
|
||||||
public const string InvoicePaymentFailed = "invoice.payment_failed";
|
|
||||||
public const string CheckoutSessionCompleted = "checkout.session.completed";
|
|
||||||
}
|
|
||||||
|
|
||||||
public class StripeWebhookEndpoint(
|
public class StripeWebhookEndpoint(
|
||||||
MembershipDbContext dbContext,
|
|
||||||
StripeService stripeService,
|
StripeService stripeService,
|
||||||
IOptions<StripeOptions> options)
|
IOptions<StripeOptions> options)
|
||||||
: EndpointWithoutRequest
|
: EndpointWithoutRequest
|
||||||
@@ -37,33 +26,24 @@ public class StripeWebhookEndpoint(
|
|||||||
|
|
||||||
switch (stripeEvent.Type)
|
switch (stripeEvent.Type)
|
||||||
{
|
{
|
||||||
case StripeEvents.InvoicePaymentSucceeded:
|
case "checkout.session.completed":
|
||||||
await stripeService.HandlePaymentSucceeded(stripeEvent, ct);
|
|
||||||
break;
|
|
||||||
case StripeEvents.InvoicePaymentFailed:
|
|
||||||
await stripeService.HandlePaymentFailed(stripeEvent, ct);
|
|
||||||
break;
|
|
||||||
case StripeEvents.CheckoutSessionCompleted:
|
|
||||||
await stripeService.HandleCheckoutSessionCompleted(stripeEvent, ct);
|
await stripeService.HandleCheckoutSessionCompleted(stripeEvent, ct);
|
||||||
break;
|
break;
|
||||||
case StripeEvents.CustomerSubscriptionDeleted:
|
case "invoice.payment_succeeded":
|
||||||
{
|
await stripeService.HandleInvoicePaymentSucceeded(stripeEvent, ct);
|
||||||
var subscription = stripeEvent.Data.Object as Stripe.Subscription;
|
break;
|
||||||
var existingSubscription = await dbContext
|
case "invoice.payment_failed":
|
||||||
.Subscriptions
|
await stripeService.HandleInvoicePaymentFailed(stripeEvent, ct);
|
||||||
.FirstOrDefaultAsync(x => x.StripeSubscriptionId == subscription!.Id, ct);
|
break;
|
||||||
|
case "customer.subscription.created":
|
||||||
if (existingSubscription != null)
|
await stripeService.HandleCustomerSubscriptionCreated(stripeEvent, ct);
|
||||||
{
|
break;
|
||||||
var today = DateTime.Today;
|
case "customer.subscription.updated":
|
||||||
int lastDay = DateTime.DaysInMonth(today.Year, today.Month);
|
await stripeService.HandleCustomerSubscriptionUpdated(stripeEvent, ct);
|
||||||
var lastDayOfMonth = new DateTime(today.Year, today.Month, lastDay);
|
break;
|
||||||
existingSubscription.EndDate = new DateTimeOffset(lastDayOfMonth);
|
case "customer.subscription.deleted":
|
||||||
await dbContext.SaveChangesAsync(ct);
|
await stripeService.HandleCustomerSubscriptionDeleted(stripeEvent, ct);
|
||||||
}
|
break;
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await SendOkAsync(ct);
|
await SendOkAsync(ct);
|
||||||
|
|||||||
@@ -1,27 +1,22 @@
|
|||||||
using Hutopy.Web.Common;
|
using Hutopy.Web.Common.Security;
|
||||||
using Hutopy.Web.Common.Security;
|
|
||||||
using Hutopy.Web.Features.Memberships.Data;
|
using Hutopy.Web.Features.Memberships.Data;
|
||||||
using Hutopy.Web.Features.Memberships.Services;
|
using Hutopy.Web.Features.Memberships.Infrastructure;
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Memberships.Handlers;
|
namespace Hutopy.Web.Features.Memberships.Handlers;
|
||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public record SendTipRequest
|
public record SendTipRequest(
|
||||||
{
|
Guid CreatorId,
|
||||||
public Guid CreatorId { get; set; }
|
decimal Amount,
|
||||||
public required decimal Amount { get; init; }
|
string Currency,
|
||||||
public required string Currency { get; init; }
|
string Message,
|
||||||
public required string Message { get; init; }
|
string CheckoutSuccessUrl,
|
||||||
public required string CheckoutSuccessUrl { get; init; }
|
string CheckoutCancelledUrl);
|
||||||
public required string CheckoutCancelledUrl { get; init; }
|
|
||||||
}
|
|
||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public class SendTipResponse
|
public record SendTipResponse(
|
||||||
{
|
string Status,
|
||||||
public required string Status { get; init; }
|
string StripeCheckoutUrl);
|
||||||
public required string StripeCheckoutUrl { get; init; }
|
|
||||||
}
|
|
||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public class SendTipRequestValidator : Validator<SendTipRequest>
|
public class SendTipRequestValidator : Validator<SendTipRequest>
|
||||||
@@ -35,11 +30,11 @@ public class SendTipRequestValidator : Validator<SendTipRequest>
|
|||||||
RuleFor(x => x.CreatorId)
|
RuleFor(x => x.CreatorId)
|
||||||
.NotEmpty()
|
.NotEmpty()
|
||||||
.WithMessage("Creator ID is required");
|
.WithMessage("Creator ID is required");
|
||||||
|
|
||||||
RuleFor(x => x.CheckoutSuccessUrl)
|
RuleFor(x => x.CheckoutSuccessUrl)
|
||||||
.NotEmpty()
|
.NotEmpty()
|
||||||
.WithMessage("CheckoutSuccessUrl is required");
|
.WithMessage("CheckoutSuccessUrl is required");
|
||||||
|
|
||||||
RuleFor(x => x.CheckoutCancelledUrl)
|
RuleFor(x => x.CheckoutCancelledUrl)
|
||||||
.NotEmpty()
|
.NotEmpty()
|
||||||
.WithMessage("CheckoutCancelledUrl is required");
|
.WithMessage("CheckoutCancelledUrl is required");
|
||||||
@@ -48,13 +43,13 @@ public class SendTipRequestValidator : Validator<SendTipRequest>
|
|||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public class SendTipHandler(
|
public class SendTipHandler(
|
||||||
MembershipDbContext dbDbContext,
|
MembershipDbContext dbContext,
|
||||||
StripeService stripeService)
|
StripeService stripeService)
|
||||||
: Endpoint<SendTipRequest, SendTipResponse>
|
: Endpoint<SendTipRequest, SendTipResponse>
|
||||||
{
|
{
|
||||||
public override void Configure()
|
public override void Configure()
|
||||||
{
|
{
|
||||||
Post("/api/tips/{CreatorId}");
|
Post("/api/tips");
|
||||||
Options(o => o.WithTags("Memberships"));
|
Options(o => o.WithTags("Memberships"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,57 +57,30 @@ public class SendTipHandler(
|
|||||||
SendTipRequest req,
|
SendTipRequest req,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
var userId = User.GetUserId();
|
var creator = await dbContext.Creators.FindAsync(
|
||||||
var userName = User.GetName();
|
|
||||||
|
|
||||||
var creator = await dbDbContext.Creators.FindAsync(
|
|
||||||
[req.CreatorId],
|
[req.CreatorId],
|
||||||
cancellationToken: ct);
|
cancellationToken: ct);
|
||||||
|
|
||||||
if (creator == null)
|
if (creator == null)
|
||||||
{
|
{
|
||||||
await SendNotFoundAsync(ct);
|
await SendNotFoundAsync(ct);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var checkoutSession = await stripeService.CreateTipCheckoutSession(
|
var checkoutSession = await stripeService.CreateTipCheckoutSessionAsync(
|
||||||
userId,
|
User.GetUserId(),
|
||||||
req.Amount,
|
User.GetAlias()!,
|
||||||
req.Currency,
|
|
||||||
creator.Id,
|
creator.Id,
|
||||||
creator.Name,
|
creator.Name,
|
||||||
|
req.Amount,
|
||||||
|
req.Currency,
|
||||||
|
req.Message,
|
||||||
creator.StripeAccountId,
|
creator.StripeAccountId,
|
||||||
req.CheckoutSuccessUrl,
|
req.CheckoutSuccessUrl,
|
||||||
req.CheckoutCancelledUrl);
|
req.CheckoutCancelledUrl);
|
||||||
|
|
||||||
dbDbContext.Tips.Add(
|
|
||||||
new Tip
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(),
|
|
||||||
CreatedAt = DateTimeOffset.UtcNow,
|
|
||||||
TipperId = userId,
|
|
||||||
TipperName = userName,
|
|
||||||
CreatorId = creator.Id,
|
|
||||||
CreatorName = creator.Name,
|
|
||||||
Amount = req.Amount,
|
|
||||||
Currency = req.Currency,
|
|
||||||
Message = req.Message,
|
|
||||||
StripeSessionId = checkoutSession.Id
|
|
||||||
});
|
|
||||||
|
|
||||||
dbDbContext.Transactions.Add(
|
|
||||||
new Transaction
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(),
|
|
||||||
StripeCheckoutSessionId = checkoutSession.Id,
|
|
||||||
Amount = req.Amount,
|
|
||||||
Type = "Tip",
|
|
||||||
Timestamp = DateTime.UtcNow
|
|
||||||
});
|
|
||||||
|
|
||||||
await dbDbContext.SaveChangesAsync(ct);
|
|
||||||
|
|
||||||
await SendAsync(
|
await SendAsync(
|
||||||
new SendTipResponse { Status = "Pending", StripeCheckoutUrl = checkoutSession.Url },
|
new SendTipResponse("Pending", checkoutSession.Url),
|
||||||
cancellation: ct);
|
cancellation: ct);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
using Hutopy.Web.Common;
|
using Hutopy.Web.Common.Security;
|
||||||
using Hutopy.Web.Common.Security;
|
|
||||||
using Hutopy.Web.Features.Memberships.Data;
|
using Hutopy.Web.Features.Memberships.Data;
|
||||||
using Hutopy.Web.Features.Memberships.Services;
|
using Hutopy.Web.Features.Memberships.Infrastructure;
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Memberships.Handlers;
|
namespace Hutopy.Web.Features.Memberships.Handlers;
|
||||||
|
|
||||||
@@ -10,17 +9,13 @@ public class SubscribeRequest
|
|||||||
{
|
{
|
||||||
public Guid CreatorId { get; set; }
|
public Guid CreatorId { get; set; }
|
||||||
public Guid TierId { get; set; }
|
public Guid TierId { get; set; }
|
||||||
|
public required string CheckoutSuccessUrl { get; init; }
|
||||||
|
public required string CheckoutCancelledUrl { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public record struct SubscriptionResponse(
|
public record struct SubscriptionResponse(
|
||||||
Guid SubscriptionId,
|
string StripeCheckoutUrl);
|
||||||
Guid CreatorId,
|
|
||||||
Guid UserId,
|
|
||||||
bool IsActive,
|
|
||||||
string Tier,
|
|
||||||
DateTimeOffset StartDate,
|
|
||||||
DateTimeOffset? EndDate);
|
|
||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public class SubscribeValidator : Validator<SubscribeRequest>
|
public class SubscribeValidator : Validator<SubscribeRequest>
|
||||||
@@ -33,7 +28,7 @@ public class SubscribeValidator : Validator<SubscribeRequest>
|
|||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public class SubscribeHandler(
|
public class SubscribeHandler(
|
||||||
MembershipDbContext dbDbContext,
|
MembershipDbContext dbContext,
|
||||||
StripeService stripeService)
|
StripeService stripeService)
|
||||||
: Endpoint<SubscribeRequest, SubscriptionResponse>
|
: Endpoint<SubscribeRequest, SubscriptionResponse>
|
||||||
{
|
{
|
||||||
@@ -47,12 +42,12 @@ public class SubscribeHandler(
|
|||||||
SubscribeRequest req,
|
SubscribeRequest req,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
var tier = await dbDbContext
|
var tier = await dbContext
|
||||||
.Tiers
|
.Tiers
|
||||||
.Include(tier => tier.Creator) // Include the related table
|
.Include(tier => tier.Creator) // Include the related table
|
||||||
.Where(tier => tier.Id == req.TierId)
|
.Where(tier => tier.Id == req.TierId)
|
||||||
.FirstOrDefaultAsync(ct);
|
.FirstOrDefaultAsync(ct);
|
||||||
|
|
||||||
if (tier == null)
|
if (tier == null)
|
||||||
{
|
{
|
||||||
await SendNotFoundAsync(ct);
|
await SendNotFoundAsync(ct);
|
||||||
@@ -60,52 +55,18 @@ public class SubscribeHandler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Process Stripe subscription
|
// Process Stripe subscription
|
||||||
var stripeSubscription = await stripeService.CreateSubscriptionCheckoutSession(
|
var checkoutSession = await stripeService.CreateSubscriptionCheckoutSession(
|
||||||
User.GetUserId(),
|
User.GetUserId(),
|
||||||
tier.Price,
|
tier.Creator.Id,
|
||||||
tier.CurrencyCode,
|
tier.Creator.Name,
|
||||||
$"{tier.Name} from {tier.Creator.Name}",
|
|
||||||
tier.Creator.StripeAccountId,
|
tier.Creator.StripeAccountId,
|
||||||
"",
|
tier.Id,
|
||||||
"");
|
tier.StripePriceId,
|
||||||
|
req.CheckoutSuccessUrl,
|
||||||
// Record subscription and transaction
|
req.CheckoutCancelledUrl);
|
||||||
var subscription = new Subscription
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(),
|
|
||||||
StripeSubscriptionId = stripeSubscription.Id,
|
|
||||||
CreatorId = tier.CreatorId,
|
|
||||||
UserId = User.GetUserId(),
|
|
||||||
Tier = tier,
|
|
||||||
StartDate = DateTimeOffset.Now,
|
|
||||||
EndDate = DateTimeOffset.Now.AddMonths(1)
|
|
||||||
};
|
|
||||||
|
|
||||||
dbDbContext.Subscriptions.Add(subscription);
|
|
||||||
|
|
||||||
dbDbContext.Transactions.Add(
|
|
||||||
new Transaction
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(),
|
|
||||||
StripeCheckoutSessionId = stripeSubscription.Id,
|
|
||||||
Amount = tier.Price,
|
|
||||||
Type = "Subscription",
|
|
||||||
Timestamp = DateTime.UtcNow
|
|
||||||
});
|
|
||||||
|
|
||||||
await dbDbContext.SaveChangesAsync(ct);
|
|
||||||
|
|
||||||
await SendOkAsync(
|
await SendOkAsync(
|
||||||
new SubscriptionResponse
|
new SubscriptionResponse { StripeCheckoutUrl = checkoutSession.Url },
|
||||||
{
|
cancellation: ct);
|
||||||
UserId = subscription.UserId,
|
|
||||||
CreatorId = subscription.CreatorId,
|
|
||||||
SubscriptionId = subscription.Id,
|
|
||||||
IsActive = subscription.IsActive,
|
|
||||||
StartDate = subscription.StartDate,
|
|
||||||
EndDate = subscription.EndDate,
|
|
||||||
Tier = tier.Name,
|
|
||||||
},
|
|
||||||
ct);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Hutopy.Web.Features.Memberships.Services;
|
namespace Hutopy.Web.Features.Memberships.Infrastructure;
|
||||||
|
|
||||||
public sealed class PushNotificationService(
|
public sealed class PushNotificationService(
|
||||||
ILogger<PushNotificationService> logger)
|
ILogger<PushNotificationService> logger)
|
||||||
435
src/Web/Features/Memberships/Infrastructure/StripeService.cs
Normal file
435
src/Web/Features/Memberships/Infrastructure/StripeService.cs
Normal file
@@ -0,0 +1,435 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Hutopy.Web.Features.Memberships.Data;
|
||||||
|
using Hutopy.Web.Features.Memberships.Events;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Stripe;
|
||||||
|
using Stripe.Checkout;
|
||||||
|
using Subscription = Stripe.Subscription;
|
||||||
|
|
||||||
|
namespace Hutopy.Web.Features.Memberships.Infrastructure;
|
||||||
|
|
||||||
|
public class StripeOptions
|
||||||
|
{
|
||||||
|
[Required] public required string SecretKey { get; init; }
|
||||||
|
|
||||||
|
[Required] public required string WebhookSecret { get; init; }
|
||||||
|
|
||||||
|
[Required] [Range(0, 1)] public required decimal HutopyRate { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class StripeService(
|
||||||
|
IOptions<StripeOptions> paymentOptions,
|
||||||
|
MembershipDbContext dbContext,
|
||||||
|
PushNotificationService notificationService)
|
||||||
|
{
|
||||||
|
public async Task<string> CreateProductAsync(
|
||||||
|
Guid creatorId,
|
||||||
|
Guid tierId,
|
||||||
|
string productName,
|
||||||
|
string currencyCode,
|
||||||
|
decimal amount)
|
||||||
|
{
|
||||||
|
StripeConfiguration.ApiKey = paymentOptions.Value.SecretKey;
|
||||||
|
|
||||||
|
// Create the product
|
||||||
|
var productService = new ProductService();
|
||||||
|
var product = await productService.CreateAsync(
|
||||||
|
new ProductCreateOptions
|
||||||
|
{
|
||||||
|
Name = productName,
|
||||||
|
Metadata = { { "creatorId", creatorId.ToString() }, { "tierId", tierId.ToString() } }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create the price for the product
|
||||||
|
var priceService = new PriceService();
|
||||||
|
await priceService.CreateAsync(
|
||||||
|
new PriceCreateOptions
|
||||||
|
{
|
||||||
|
Product = product.Id,
|
||||||
|
UnitAmountDecimal = amount * 100, // Convert amount to cents
|
||||||
|
Currency = currencyCode,
|
||||||
|
Recurring = new PriceRecurringOptions { Interval = "month" }
|
||||||
|
});
|
||||||
|
|
||||||
|
return product.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Session> CreateTipCheckoutSessionAsync(
|
||||||
|
Guid tipperId,
|
||||||
|
string tipperName,
|
||||||
|
Guid creatorId,
|
||||||
|
string creatorName,
|
||||||
|
decimal amount,
|
||||||
|
string currencyCode,
|
||||||
|
string message,
|
||||||
|
string creatorAccountId,
|
||||||
|
string successUrl,
|
||||||
|
string cancelUrl,
|
||||||
|
CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
StripeConfiguration.ApiKey = paymentOptions.Value.SecretKey;
|
||||||
|
|
||||||
|
// Create Stripe customer for the user if not already created
|
||||||
|
var customerService = new CustomerService();
|
||||||
|
var customer = await customerService.CreateAsync(
|
||||||
|
new CustomerCreateOptions
|
||||||
|
{
|
||||||
|
Metadata = new Dictionary<string, string> { { "userId", tipperId.ToString() } }
|
||||||
|
},
|
||||||
|
cancellationToken: ct);
|
||||||
|
|
||||||
|
// Create paymentIntent for the user
|
||||||
|
var sessionService = new SessionService();
|
||||||
|
return await sessionService.CreateAsync(
|
||||||
|
new SessionCreateOptions
|
||||||
|
{
|
||||||
|
Customer = customer.Id,
|
||||||
|
PaymentMethodTypes = ["card"],
|
||||||
|
LineItems =
|
||||||
|
[
|
||||||
|
new SessionLineItemOptions
|
||||||
|
{
|
||||||
|
PriceData = new SessionLineItemPriceDataOptions
|
||||||
|
{
|
||||||
|
Currency = currencyCode,
|
||||||
|
UnitAmountDecimal = amount, // Amount in cents
|
||||||
|
ProductData = new SessionLineItemPriceDataProductDataOptions
|
||||||
|
{
|
||||||
|
Name = $"Tip for {creatorName}", // or any descriptive name for the tip
|
||||||
|
Metadata = new Dictionary<string, string> { { "creatorId", creatorId.ToString() } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Quantity = 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
Mode = "payment",
|
||||||
|
PaymentIntentData = new SessionPaymentIntentDataOptions
|
||||||
|
{
|
||||||
|
ApplicationFeeAmount =
|
||||||
|
Convert.ToInt64(amount * 100 * paymentOptions.Value.HutopyRate), // Platform fee
|
||||||
|
TransferData = new SessionPaymentIntentDataTransferDataOptions
|
||||||
|
{
|
||||||
|
Destination = creatorAccountId // Creator's Stripe account ID
|
||||||
|
}
|
||||||
|
},
|
||||||
|
SuccessUrl = successUrl, // Redirect after successful payment
|
||||||
|
CancelUrl = cancelUrl, // Redirect after canceled payment
|
||||||
|
Metadata = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "tipperId", tipperId.ToString() },
|
||||||
|
{ "tipperName", tipperName },
|
||||||
|
{ "creatorId", creatorId.ToString() },
|
||||||
|
{ "creatorName", creatorName },
|
||||||
|
{ "message", message },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cancellationToken: ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Session> CreateSubscriptionCheckoutSession(
|
||||||
|
Guid userId,
|
||||||
|
Guid creatorId,
|
||||||
|
string creatorName,
|
||||||
|
string creatorAccountId,
|
||||||
|
Guid tierId,
|
||||||
|
string priceId,
|
||||||
|
string successUrl,
|
||||||
|
string cancelUrl)
|
||||||
|
{
|
||||||
|
StripeConfiguration.ApiKey = paymentOptions.Value.SecretKey;
|
||||||
|
|
||||||
|
// Create Stripe customer for the user if not already created
|
||||||
|
var customerService = new CustomerService();
|
||||||
|
var customer = await customerService.CreateAsync(
|
||||||
|
new CustomerCreateOptions
|
||||||
|
{
|
||||||
|
Metadata = new Dictionary<string, string> { { "userId", userId.ToString() } }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create Checkout Session for the subscription
|
||||||
|
var sessionService = new SessionService();
|
||||||
|
return await sessionService.CreateAsync(
|
||||||
|
new SessionCreateOptions
|
||||||
|
{
|
||||||
|
Customer = customer.Id,
|
||||||
|
PaymentMethodTypes = ["card"],
|
||||||
|
LineItems =
|
||||||
|
[
|
||||||
|
new SessionLineItemOptions { Price = priceId, Quantity = 1 }
|
||||||
|
],
|
||||||
|
Mode = "subscription",
|
||||||
|
SubscriptionData = new SessionSubscriptionDataOptions
|
||||||
|
{
|
||||||
|
ApplicationFeePercent = paymentOptions.Value.HutopyRate,
|
||||||
|
TransferData = new SessionSubscriptionDataTransferDataOptions { Destination = creatorAccountId }
|
||||||
|
},
|
||||||
|
SuccessUrl = successUrl, // Redirect after successful payment
|
||||||
|
CancelUrl = cancelUrl, // Redirect after canceled payment
|
||||||
|
Metadata = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "userId", userId.ToString() },
|
||||||
|
{ "creatorId", creatorId.ToString() },
|
||||||
|
{ "creatorName", creatorName },
|
||||||
|
{ "tierId", tierId.ToString() }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CancelSubscription(
|
||||||
|
Guid subscriptionId)
|
||||||
|
{
|
||||||
|
var subscriptionService = new SubscriptionService();
|
||||||
|
await subscriptionService.CancelAsync(subscriptionId.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task HandleInvoicePaymentSucceeded(
|
||||||
|
Event stripeEvent,
|
||||||
|
CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
// Ensure we have an invoice related to a Subscription
|
||||||
|
if (stripeEvent.Data.Object is not Invoice { Subscription: not null } invoice)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var subscription = await dbContext
|
||||||
|
.Subscriptions
|
||||||
|
.FirstOrDefaultAsync(
|
||||||
|
subscription => subscription.StripeSubscriptionId == invoice.Subscription.Id,
|
||||||
|
cancellationToken: ct);
|
||||||
|
|
||||||
|
if (subscription == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record the Transaction
|
||||||
|
var transaction = new Transaction
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
CreatedAt = DateTimeOffset.UtcNow,
|
||||||
|
Amount = invoice.AmountPaid / 100m, // Convert amount from cents to dollars
|
||||||
|
Currency = invoice.Currency,
|
||||||
|
Type = "Subscription",
|
||||||
|
Timestamp = DateTime.UtcNow,
|
||||||
|
StripeInvoiceUrl = invoice.HostedInvoiceUrl
|
||||||
|
};
|
||||||
|
|
||||||
|
dbContext.Transactions.Add(transaction);
|
||||||
|
|
||||||
|
// Link the Transaction to the Subscription
|
||||||
|
subscription.Transactions.Add(transaction);
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task HandleInvoicePaymentFailed(
|
||||||
|
Event stripeEvent,
|
||||||
|
CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
if (stripeEvent.Data.Object is not Invoice { Subscription: not null } invoice)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var subscription = await dbContext
|
||||||
|
.Subscriptions
|
||||||
|
.SingleOrDefaultAsync(
|
||||||
|
subscription => subscription.StripeSubscriptionId == invoice.SubscriptionId,
|
||||||
|
cancellationToken: ct);
|
||||||
|
|
||||||
|
if (subscription != null)
|
||||||
|
{
|
||||||
|
subscription.EndDate = DateTimeOffset.UtcNow; // Mark as expired or failed
|
||||||
|
await dbContext.SaveChangesAsync(ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleTipPayment(
|
||||||
|
Session session,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
// Record the Tip
|
||||||
|
var tip = new Tip
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
CreatedAt = DateTimeOffset.UtcNow,
|
||||||
|
StripeSessionId = session.Id,
|
||||||
|
TipperId = Guid.Parse(session.Metadata["tipperId"]),
|
||||||
|
TipperName = session.Metadata["tipperName"],
|
||||||
|
CreatorId = Guid.Parse(session.Metadata["creatorId"]),
|
||||||
|
CreatorName = session.Metadata["creatorName"],
|
||||||
|
Amount = session.AmountTotal!.Value / 100m,
|
||||||
|
Currency = session.Currency,
|
||||||
|
Message = session.Metadata["message"]
|
||||||
|
};
|
||||||
|
|
||||||
|
dbContext.Tips.Add(tip);
|
||||||
|
|
||||||
|
// Record the Transaction
|
||||||
|
var transaction = new Transaction
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
CreatedAt = DateTimeOffset.UtcNow,
|
||||||
|
Amount = tip.Amount,
|
||||||
|
Currency = tip.Currency,
|
||||||
|
Type = "Tip",
|
||||||
|
Timestamp = DateTime.UtcNow,
|
||||||
|
// TODO: __StripeInvoiceUrl = session.Invoice.HostedInvoiceUrl__ How come nor Invoice or InvoiceId are set.
|
||||||
|
};
|
||||||
|
|
||||||
|
dbContext.Transactions.Add(transaction);
|
||||||
|
|
||||||
|
// Link the Transaction to the Tip
|
||||||
|
tip.TransactionId = transaction.Id;
|
||||||
|
|
||||||
|
// Save the changes
|
||||||
|
await dbContext.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
// Notify the Creator
|
||||||
|
notificationService.NotifyCreator(
|
||||||
|
tip.CreatorId,
|
||||||
|
new TipPaid(
|
||||||
|
tip.CreatorId,
|
||||||
|
tip.CreatorName,
|
||||||
|
tip.Amount,
|
||||||
|
tip.Currency,
|
||||||
|
tip.Message)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleSubscriptionPayment(
|
||||||
|
Session session,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
// Record the Subscription
|
||||||
|
var subscription = new Data.Subscription
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
CreatedAt = DateTimeOffset.UtcNow,
|
||||||
|
UserId = Guid.Parse(session.Metadata["userId"]),
|
||||||
|
CreatorId = Guid.Parse(session.Metadata["creatorId"]),
|
||||||
|
TierId = Guid.Parse(session.Metadata["tierId"]),
|
||||||
|
StartDate = DateTimeOffset.UtcNow,
|
||||||
|
StripeSessionId = session.Id,
|
||||||
|
StripeSubscriptionId = session.SubscriptionId
|
||||||
|
};
|
||||||
|
|
||||||
|
dbContext.Subscriptions.Add(subscription);
|
||||||
|
|
||||||
|
// Record the Transaction
|
||||||
|
var transaction = new Transaction
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
CreatedAt = DateTimeOffset.UtcNow,
|
||||||
|
Amount = session.AmountTotal!.Value / 100m, // Convert amount from cents to dollars
|
||||||
|
Currency = session.Currency,
|
||||||
|
Type = "Subscription",
|
||||||
|
Timestamp = DateTime.UtcNow,
|
||||||
|
// TODO: __StripeInvoiceUrl = session.Invoice.HostedInvoiceUrl__ How come nor Invoice or InvoiceId are set.
|
||||||
|
};
|
||||||
|
|
||||||
|
dbContext.Transactions.Add(transaction);
|
||||||
|
|
||||||
|
// Link the Transaction to the Subscription
|
||||||
|
subscription.Transactions.Add(transaction);
|
||||||
|
|
||||||
|
// Save the changes
|
||||||
|
await dbContext.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
// Notify the Creator
|
||||||
|
notificationService.NotifyCreator(
|
||||||
|
subscription.CreatorId,
|
||||||
|
new SubscriptionPaid(
|
||||||
|
subscription.CreatorId,
|
||||||
|
session.Metadata["creatorName"],
|
||||||
|
subscription.TierId.ToString(),
|
||||||
|
subscription.StartDate)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task HandleCheckoutSessionCompleted(
|
||||||
|
Event stripeEvent,
|
||||||
|
CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
if (stripeEvent.Data.Object is not Session session)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (session.Mode)
|
||||||
|
{
|
||||||
|
// Check if this is a one-time tip
|
||||||
|
case "payment" when session.PaymentIntentId != null:
|
||||||
|
await HandleTipPayment(session, ct);
|
||||||
|
break;
|
||||||
|
// Check if this is a subscription
|
||||||
|
case "subscription" when session.SubscriptionId != null:
|
||||||
|
await HandleSubscriptionPayment(session, ct);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task HandleCustomerSubscriptionCreated(
|
||||||
|
Event stripeEvent,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (stripeEvent.Data.Object is not Subscription stripeSubscription)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var subscription = await dbContext
|
||||||
|
.Subscriptions
|
||||||
|
.SingleOrDefaultAsync(
|
||||||
|
subscription => subscription.StripeSubscriptionId == stripeSubscription.Id,
|
||||||
|
cancellationToken: ct);
|
||||||
|
|
||||||
|
if (subscription != null)
|
||||||
|
{
|
||||||
|
subscription.StartDate = stripeSubscription.CurrentPeriodStart;
|
||||||
|
subscription.EndDate = null; // Active subscription
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync(ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task HandleCustomerSubscriptionUpdated(
|
||||||
|
Event stripeEvent,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (stripeEvent.Data.Object is Subscription stripeSubscription)
|
||||||
|
{
|
||||||
|
var subscription = await dbContext
|
||||||
|
.Subscriptions
|
||||||
|
.SingleOrDefaultAsync(
|
||||||
|
s => s.StripeSubscriptionId == stripeSubscription.Id,
|
||||||
|
cancellationToken: ct);
|
||||||
|
|
||||||
|
if (subscription != null)
|
||||||
|
{
|
||||||
|
subscription.StartDate = stripeSubscription.CurrentPeriodStart;
|
||||||
|
subscription.EndDate = null; // Active subscription
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync(ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task HandleCustomerSubscriptionDeleted(
|
||||||
|
Event stripeEvent,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
var subscription = stripeEvent.Data.Object as Subscription;
|
||||||
|
var existingSubscription = await dbContext
|
||||||
|
.Subscriptions
|
||||||
|
.FirstOrDefaultAsync(x => x.StripeSubscriptionId == subscription!.Id, ct);
|
||||||
|
|
||||||
|
if (existingSubscription != null)
|
||||||
|
{
|
||||||
|
var today = DateTime.Today;
|
||||||
|
int lastDay = DateTime.DaysInMonth(today.Year, today.Month);
|
||||||
|
var lastDayOfMonth = new DateTime(today.Year, today.Month, lastDay);
|
||||||
|
existingSubscription.EndDate = new DateTimeOffset(lastDayOfMonth);
|
||||||
|
await dbContext.SaveChangesAsync(ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,224 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using Hutopy.Web.Features.Memberships.Data;
|
|
||||||
using Hutopy.Web.Features.Memberships.Events;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using Stripe;
|
|
||||||
using Stripe.Checkout;
|
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Memberships.Services;
|
|
||||||
|
|
||||||
public class StripeOptions
|
|
||||||
{
|
|
||||||
[Required] public required string SecretKey { get; init; }
|
|
||||||
|
|
||||||
[Required] public required string WebhookSecret { get; init; }
|
|
||||||
|
|
||||||
[Range(0, 1)] public required decimal HutopyRate { get; init; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class StripeService(
|
|
||||||
IOptions<StripeOptions> paymentOptions,
|
|
||||||
MembershipDbContext dbDbContext,
|
|
||||||
PushNotificationService notificationService)
|
|
||||||
{
|
|
||||||
public async Task<Session> CreateTipCheckoutSession(
|
|
||||||
Guid userId,
|
|
||||||
decimal amount,
|
|
||||||
string currencyCode,
|
|
||||||
Guid creatorId,
|
|
||||||
string creatorName,
|
|
||||||
string creatorAccountId,
|
|
||||||
string successUrl,
|
|
||||||
string cancelUrl)
|
|
||||||
{
|
|
||||||
StripeConfiguration.ApiKey = paymentOptions.Value.SecretKey;
|
|
||||||
|
|
||||||
// Create Stripe customer for the user if not already created
|
|
||||||
var customerService = new CustomerService();
|
|
||||||
var customer = await customerService.CreateAsync(
|
|
||||||
new CustomerCreateOptions
|
|
||||||
{
|
|
||||||
Metadata = new Dictionary<string, string> { { "userId", userId.ToString() } }
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create paymentIntent for the user
|
|
||||||
var sessionService = new SessionService();
|
|
||||||
return await sessionService.CreateAsync(
|
|
||||||
new SessionCreateOptions
|
|
||||||
{
|
|
||||||
Customer = customer.Id,
|
|
||||||
PaymentMethodTypes = ["card"],
|
|
||||||
LineItems =
|
|
||||||
[
|
|
||||||
new SessionLineItemOptions
|
|
||||||
{
|
|
||||||
PriceData = new SessionLineItemPriceDataOptions
|
|
||||||
{
|
|
||||||
Currency = currencyCode,
|
|
||||||
UnitAmountDecimal = amount, // Amount in cents
|
|
||||||
ProductData = new SessionLineItemPriceDataProductDataOptions
|
|
||||||
{
|
|
||||||
Name = $"Tip for {creatorName}", // or any descriptive name for the tip
|
|
||||||
Metadata = new Dictionary<string, string> { { "creatorId", creatorId.ToString() } }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Quantity = 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
Mode = "payment",
|
|
||||||
PaymentIntentData = new SessionPaymentIntentDataOptions
|
|
||||||
{
|
|
||||||
ApplicationFeeAmount =
|
|
||||||
Convert.ToInt64(amount * 100 * paymentOptions.Value.HutopyRate), // Platform fee
|
|
||||||
TransferData = new SessionPaymentIntentDataTransferDataOptions
|
|
||||||
{
|
|
||||||
Destination = creatorAccountId // Creator's Stripe account ID
|
|
||||||
}
|
|
||||||
},
|
|
||||||
SuccessUrl = successUrl, // Redirect after successful payment
|
|
||||||
CancelUrl = cancelUrl // Redirect after canceled payment
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Session> CreateSubscriptionCheckoutSession(
|
|
||||||
Guid userId,
|
|
||||||
decimal amount,
|
|
||||||
string currencyCode,
|
|
||||||
string productName,
|
|
||||||
string creatorAccountId,
|
|
||||||
string successUrl,
|
|
||||||
string cancelUrl)
|
|
||||||
{
|
|
||||||
// Create Stripe customer for the user if not already created
|
|
||||||
var customerService = new CustomerService();
|
|
||||||
var customer = await customerService.CreateAsync(
|
|
||||||
new CustomerCreateOptions
|
|
||||||
{
|
|
||||||
Metadata = new Dictionary<string, string> { { "userId", userId.ToString() } }
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create Checkout Session for the subscription
|
|
||||||
var sessionService = new SessionService();
|
|
||||||
return await sessionService.CreateAsync(new SessionCreateOptions
|
|
||||||
{
|
|
||||||
Customer = customer.Id,
|
|
||||||
PaymentMethodTypes = ["card"],
|
|
||||||
LineItems =
|
|
||||||
[
|
|
||||||
new SessionLineItemOptions
|
|
||||||
{
|
|
||||||
PriceData = new SessionLineItemPriceDataOptions
|
|
||||||
{
|
|
||||||
Currency = currencyCode,
|
|
||||||
Recurring = new SessionLineItemPriceDataRecurringOptions { Interval = "month" },
|
|
||||||
UnitAmountDecimal = amount, // Amount in cents
|
|
||||||
ProductData = new SessionLineItemPriceDataProductDataOptions { Name = productName }
|
|
||||||
},
|
|
||||||
Quantity = 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
Mode = "subscription",
|
|
||||||
SubscriptionData = new SessionSubscriptionDataOptions
|
|
||||||
{
|
|
||||||
ApplicationFeePercent = paymentOptions.Value.HutopyRate, // Platform fee as a percentage
|
|
||||||
TransferData = new SessionSubscriptionDataTransferDataOptions
|
|
||||||
{
|
|
||||||
Destination = creatorAccountId // Creator's Stripe account ID
|
|
||||||
}
|
|
||||||
},
|
|
||||||
SuccessUrl = successUrl, // Redirect after successful payment
|
|
||||||
CancelUrl = cancelUrl // Redirect after canceled payment
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task CancelSubscription(
|
|
||||||
Guid subscriptionId)
|
|
||||||
{
|
|
||||||
var subscriptionService = new SubscriptionService();
|
|
||||||
await subscriptionService.CancelAsync(subscriptionId.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task HandlePaymentSucceeded(
|
|
||||||
Event stripeEvent,
|
|
||||||
CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
var invoice = stripeEvent.Data.Object as Invoice;
|
|
||||||
var subscriptionId = invoice.SubscriptionId;
|
|
||||||
|
|
||||||
var subscription = await dbDbContext
|
|
||||||
.Subscriptions
|
|
||||||
.FirstOrDefaultAsync(x => x.StripeSubscriptionId == subscriptionId, ct);
|
|
||||||
|
|
||||||
if (subscription != null)
|
|
||||||
{
|
|
||||||
subscription.EndDate = null;
|
|
||||||
await dbDbContext.SaveChangesAsync(ct);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task HandlePaymentFailed(
|
|
||||||
Event stripeEvent,
|
|
||||||
CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
var invoice = stripeEvent.Data.Object as Invoice;
|
|
||||||
var subscriptionId = invoice!.SubscriptionId;
|
|
||||||
|
|
||||||
var subscription = await dbDbContext
|
|
||||||
.Subscriptions
|
|
||||||
.FirstOrDefaultAsync(x => x.StripeSubscriptionId == subscriptionId, ct);
|
|
||||||
|
|
||||||
if (subscription != null)
|
|
||||||
{
|
|
||||||
var today = DateTime.Today;
|
|
||||||
var lastDay = DateTime.DaysInMonth(today.Year, today.Month);
|
|
||||||
var lastDayOfMonth = new DateTime(today.Year, today.Month, lastDay);
|
|
||||||
subscription.EndDate = lastDayOfMonth;
|
|
||||||
await dbDbContext.SaveChangesAsync(ct);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task HandleCheckoutSessionCompleted(
|
|
||||||
Event stripeEvent,
|
|
||||||
CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
var session = stripeEvent.Data.Object as Session;
|
|
||||||
var sessionId = session!.Id;
|
|
||||||
|
|
||||||
var tip = await dbDbContext
|
|
||||||
.Tips
|
|
||||||
.Where(tip => tip.StripeSessionId == sessionId)
|
|
||||||
.SingleOrDefaultAsync(ct);
|
|
||||||
|
|
||||||
if (tip is not null)
|
|
||||||
{
|
|
||||||
notificationService.NotifyCreator(
|
|
||||||
tip.CreatorId,
|
|
||||||
new TipPaid(
|
|
||||||
tip.CreatorId,
|
|
||||||
tip.CreatorName,
|
|
||||||
tip.Amount,
|
|
||||||
tip.Currency,
|
|
||||||
tip.Message));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var subscription = await dbDbContext
|
|
||||||
.Subscriptions
|
|
||||||
.Where(subscription => subscription.StripeSessionId == sessionId)
|
|
||||||
.Include(subscription => subscription.Tier)
|
|
||||||
.Include(subscription => subscription.Creator)
|
|
||||||
.SingleOrDefaultAsync(ct);
|
|
||||||
|
|
||||||
if (subscription is not null)
|
|
||||||
{
|
|
||||||
notificationService.NotifyCreator(
|
|
||||||
subscription.CreatorId,
|
|
||||||
new SubscriptionPaid(
|
|
||||||
subscription.CreatorId,
|
|
||||||
subscription.Creator.Name,
|
|
||||||
subscription.Tier.Name,
|
|
||||||
subscription.StartDate));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -31,6 +31,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="Features\Contents\Data\Migrations\" />
|
<Folder Include="Features\Contents\Data\Migrations\" />
|
||||||
|
<Folder Include="Features\Memberships\Data\Migrations\" />
|
||||||
<Folder Include="Features\Users\Data\Migrations\" />
|
<Folder Include="Features\Users\Data\Migrations\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user