diff --git a/.gitignore b/.gitignore index 87e0e84..d6fda7d 100644 --- a/.gitignore +++ b/.gitignore @@ -485,3 +485,8 @@ $RECYCLE.BIN/ # Other IDE files .vscode/ .idea/ + + +#AppSettings dev +*/appsettings.Development.json +/src/Web/appsettings.Development.json diff --git a/Directory.Packages.props b/Directory.Packages.props index d3ae35d..b8593ab 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -40,7 +40,7 @@ - + \ No newline at end of file diff --git a/src/Application/Common/Interfaces/IStripeService.cs b/src/Application/Common/Interfaces/IStripeService.cs index 75edbe9..6737a3d 100644 --- a/src/Application/Common/Interfaces/IStripeService.cs +++ b/src/Application/Common/Interfaces/IStripeService.cs @@ -1,7 +1,11 @@ +using Hutopy.Application.Common.Models; +using Hutopy.Application.Stripe.Commands; + namespace Hutopy.Application.Common.Interfaces; public interface IStripeService { - public Task CreateCheckoutSession(int amount, string currency); + public Task CreateCheckoutSession(int amount, string creatorId, string currency); + public Result ValidateTransaction(ConfirmStripeTransactionCommand request); } diff --git a/src/Application/Stripe/Commands/ConfirmStripeTransaction.cs b/src/Application/Stripe/Commands/ConfirmStripeTransaction.cs index 7aa8a3e..0ddd17e 100644 --- a/src/Application/Stripe/Commands/ConfirmStripeTransaction.cs +++ b/src/Application/Stripe/Commands/ConfirmStripeTransaction.cs @@ -1,24 +1,73 @@ using Hutopy.Application.Common.Interfaces; namespace Hutopy.Application.Stripe.Commands; -public record ConfirmStripeTransactionCommand : IRequest +public class ConfirmStripeTransactionCommand : IRequest { - public required Guid UserTransactionId { get; init; } - public required bool IsConfirmed { get; init; } + public string Id { get; set; } + public string Object { get; set; } + public int Created { get; set; } + public Data Data { get; set; } + public Request Request { get; set; } +} + +public class Data +{ + public Object Object { get; set; } +} + +public class Object +{ + public string Id { get; set; } = String.Empty; + public int Amount { get; set; } + public BillingDetails Billing_details { get; set; } = new(); + public string Calculated_statement_descriptor { get; set; } = String.Empty; + public string Currency { get; set; } = String.Empty; + public bool Paid { get; set; } + public string Payment_intent { get; set; } = String.Empty; + public string Payment_method { get; set; } = String.Empty; + public string Receipt_url { get; set; } = String.Empty; + public string Status { get; set; } = String.Empty; + public string Failure_message { get; set; } = String.Empty; +} + +public class BillingDetails +{ + public string Email { get; set; } = String.Empty; + public string Name { get; set; } = String.Empty; + public string Phone { get; set; } = String.Empty; +} + +public class Request +{ + public string Id { get; set; } = String.Empty; } public class ConfirmStripeTransactionCommandHandler( - IApplicationDbContext dbContext + IApplicationDbContext dbContext, + IStripeService stripeService ) : IRequestHandler { public async Task Handle(ConfirmStripeTransactionCommand request, CancellationToken cancellationToken) { - var transaction = await dbContext.UserTransactions.FirstOrDefaultAsync(x => x.Id == request.UserTransactionId, cancellationToken); - if (transaction is null) return ""; - transaction.IsConfirmed = request.IsConfirmed; - dbContext.UserTransactions.Update(transaction); + var lastTransaction = await dbContext.UserTransactions.OrderBy(x => x.Created).LastAsync(cancellationToken); + var stripeConfirmation = stripeService.ValidateTransaction(request); - return transaction.Id.ToString(); + if (stripeConfirmation.Succeeded) + { + lastTransaction.IsConfirmed = true; + } + lastTransaction.Paid = request.Data.Object.Paid; + lastTransaction.StripeChargeId = request.Data.Object.Id; + lastTransaction.StripeEventId = request.Id; + lastTransaction.StripeReceiptUrl = request.Data.Object.Receipt_url; + lastTransaction.StripePaymentIntent = request.Data.Object.Payment_intent; + lastTransaction.StripePaymentMethod = request.Data.Object.Payment_method; + lastTransaction.StripeBillingDetailEmail = request.Data.Object.Billing_details.Email; + lastTransaction.StripeBillingDetailName = request.Data.Object.Billing_details.Name; + + await dbContext.SaveChangesAsync(cancellationToken); + + return ""; } } diff --git a/src/Application/Stripe/Commands/CreateSessionCheckoutCommand.cs b/src/Application/Stripe/Commands/CreateSessionCheckoutCommand.cs index b428733..681c4d8 100644 --- a/src/Application/Stripe/Commands/CreateSessionCheckoutCommand.cs +++ b/src/Application/Stripe/Commands/CreateSessionCheckoutCommand.cs @@ -18,13 +18,11 @@ public class CreateSessionCheckoutCommandHandler( { public async Task Handle(CreateSessionCheckoutCommand request, CancellationToken cancellationToken) { - var stripeSecret = await stripeService.CreateCheckoutSession(request.Amount, request.Currency); + var stripeSecret = await stripeService.CreateCheckoutSession(request.Amount, request.CreatorId, request.Currency); // ReSharper disable once PossibleLossOfFraction decimal priceInDollars = (request.Amount / 100); - - //todo: Need to add this transaction after the confirmation from stripe in the frontEnd ( redirect, re-call backend ) var userTransaction = new UserTransaction { Currency = request.Currency, Amount = priceInDollars, TipMessage = request.TipMessage, ApplicationUserId = request.CreatorId diff --git a/src/Application/Stripe/Queries/GetMyLastReceipt.cs b/src/Application/Stripe/Queries/GetMyLastReceipt.cs new file mode 100644 index 0000000..b8a0602 --- /dev/null +++ b/src/Application/Stripe/Queries/GetMyLastReceipt.cs @@ -0,0 +1,29 @@ +using Hutopy.Application.Common.Interfaces; + +namespace Hutopy.Application.Stripe.Queries; + +public record GetMyLastReceiptQuery : IRequest +{ + public string Email { get; set; } = string.Empty; + public string CreatorId { get; set; } = string.Empty; +}; + +public class GetMyLastReceiptQueryHandler( + IApplicationDbContext dbContext + ) + : IRequestHandler +{ + public async Task Handle(GetMyLastReceiptQuery request, CancellationToken cancellationToken) + { + var lastTransaction = await dbContext.UserTransactions.OrderBy(x => x.Created) + .LastOrDefaultAsync(x => x.ApplicationUserId == request.CreatorId && x.StripeBillingDetailEmail == request.Email, + cancellationToken); + + var receiptUrl = new MyLastReceiptDto + { + ReceiptUrl = lastTransaction?.StripeReceiptUrl ?? "", + }; + + return receiptUrl; + } +} diff --git a/src/Application/Stripe/Queries/MyLastReceiptDto.cs b/src/Application/Stripe/Queries/MyLastReceiptDto.cs new file mode 100644 index 0000000..5ce1645 --- /dev/null +++ b/src/Application/Stripe/Queries/MyLastReceiptDto.cs @@ -0,0 +1,6 @@ +namespace Hutopy.Application.Stripe.Queries; + +public class MyLastReceiptDto +{ + public string ReceiptUrl { get; set; } +} diff --git a/src/Application/Users/Queries/GetCurrentUser.cs b/src/Application/Users/Queries/GetCurrentUser/GetCurrentUser.cs similarity index 93% rename from src/Application/Users/Queries/GetCurrentUser.cs rename to src/Application/Users/Queries/GetCurrentUser/GetCurrentUser.cs index a087cf8..82dc2f8 100644 --- a/src/Application/Users/Queries/GetCurrentUser.cs +++ b/src/Application/Users/Queries/GetCurrentUser/GetCurrentUser.cs @@ -18,12 +18,11 @@ public class GetCurrentUserQueryHandler( var currentUserId = new Guid(identityUser?.Id ?? ""); var transactions = await context.UserTransactions - .Where(x => x.Id == currentUserId) + .Where(x => x.ApplicationUserId == currentUserId.ToString()) .OrderBy(x => x.LastModified) .ProjectTo(mapper.ConfigurationProvider) .ToListAsync(cancellationToken); - var user = new UserDto() { Id = currentUserId, diff --git a/src/Application/Users/Queries/UserDto.cs b/src/Application/Users/Queries/GetCurrentUser/UserDto.cs similarity index 83% rename from src/Application/Users/Queries/UserDto.cs rename to src/Application/Users/Queries/GetCurrentUser/UserDto.cs index da62e26..acece9c 100644 --- a/src/Application/Users/Queries/UserDto.cs +++ b/src/Application/Users/Queries/GetCurrentUser/UserDto.cs @@ -7,6 +7,7 @@ public class UserDto public required string FirstName { get; init; } public required string LastName { get; init; } + public string UserName { get; init; } = String.Empty; public List UserTransactions { get; init; } = []; } diff --git a/src/Application/Users/Queries/UserTransactionDto.cs b/src/Application/Users/Queries/GetCurrentUser/UserTransactionDto.cs similarity index 100% rename from src/Application/Users/Queries/UserTransactionDto.cs rename to src/Application/Users/Queries/GetCurrentUser/UserTransactionDto.cs diff --git a/src/Application/Users/Queries/GetMinimalUser/GetMinimalUser.cs b/src/Application/Users/Queries/GetMinimalUser/GetMinimalUser.cs new file mode 100644 index 0000000..4206c79 --- /dev/null +++ b/src/Application/Users/Queries/GetMinimalUser/GetMinimalUser.cs @@ -0,0 +1,28 @@ +using Hutopy.Domain.Interfaces; + +namespace Hutopy.Application.Users.Queries.GetMinimalUser; + +public record GetMinimalUserQuery : IRequest +{ + public string UserId { get; set; } = string.Empty; +}; + +public class GetMinimalUserQueryHandler( + IUserService userService + ) + : IRequestHandler +{ + public async Task Handle(GetMinimalUserQuery request, CancellationToken cancellationToken) + { + var identityUser = await userService.FindUserByIdAsync(request.UserId); + + var user = new MinimalUserDto() + { + FirstName = identityUser?.FirstName ?? "", + LastName = identityUser?.LastName ?? "", + UserName = identityUser?.UserName ?? "" + }; + + return user; + } +} diff --git a/src/Application/Users/Queries/GetMinimalUser/MinimalUserDto.cs b/src/Application/Users/Queries/GetMinimalUser/MinimalUserDto.cs new file mode 100644 index 0000000..0b01fba --- /dev/null +++ b/src/Application/Users/Queries/GetMinimalUser/MinimalUserDto.cs @@ -0,0 +1,8 @@ +namespace Hutopy.Application.Users.Queries.GetMinimalUser; + +public class MinimalUserDto +{ + public required string FirstName { get; init; } + public required string LastName { get; init; } + public string UserName { get; init; } = String.Empty; +} diff --git a/src/Domain/Entities/UserTransaction.cs b/src/Domain/Entities/UserTransaction.cs index b879fd2..78e32d4 100644 --- a/src/Domain/Entities/UserTransaction.cs +++ b/src/Domain/Entities/UserTransaction.cs @@ -7,6 +7,14 @@ public class UserTransaction : BaseAuditableEntity public string TipMessage { get; set; } = string.Empty; // Foreign key to ApplicationUser - public string ApplicationUserId { get; set; } = string.Empty; - public bool IsConfirmed { get; set; } = false; + public required string ApplicationUserId { get; set; } + public bool IsConfirmed { get; set; } + public string StripeEventId { get; set; } = string.Empty; + public string StripeChargeId { get; set; } = string.Empty; + public string StripePaymentIntent { get; set; } = string.Empty; + public string StripePaymentMethod { get; set; } = string.Empty; + public string StripeReceiptUrl { get; set; } = string.Empty; + public string StripeBillingDetailEmail { get; set; } = string.Empty; + public string StripeBillingDetailName { get; set; } = string.Empty; + public bool Paid { get; set; } } diff --git a/src/Infrastructure/Migrations/20240509215538_AddMoreInformationToTransaction.Designer.cs b/src/Infrastructure/Migrations/20240509215538_AddMoreInformationToTransaction.Designer.cs new file mode 100644 index 0000000..f8c9e81 --- /dev/null +++ b/src/Infrastructure/Migrations/20240509215538_AddMoreInformationToTransaction.Designer.cs @@ -0,0 +1,418 @@ +// +using System; +using Hutopy.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Hutopy.Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20240509215538_AddMoreInformationToTransaction")] + partial class AddMoreInformationToTransaction + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Hutopy.Domain.Entities.FutureCreator", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Created") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("EmailAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastModified") + .HasColumnType("datetimeoffset"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ReasonToJoin") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SocialNetworkAccount") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("FutureCreators"); + }); + + modelBuilder.Entity("Hutopy.Domain.Entities.UserTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ApplicationUserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("Created") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsConfirmed") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetimeoffset"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Paid") + .HasColumnType("bit"); + + b.Property("StripeBillingDetailEmail") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StripeBillingDetailName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StripeChargeId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StripeEventId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StripePaymentIntent") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StripePaymentMethod") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StripeReceiptUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TipMessage") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationUserId"); + + b.ToTable("UserTransactions"); + }); + + modelBuilder.Entity("Hutopy.Infrastructure.Identity.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("RoleId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Hutopy.Domain.Entities.UserTransaction", b => + { + b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("ApplicationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Migrations/20240509215538_AddMoreInformationToTransaction.cs b/src/Infrastructure/Migrations/20240509215538_AddMoreInformationToTransaction.cs new file mode 100644 index 0000000..d91d3cf --- /dev/null +++ b/src/Infrastructure/Migrations/20240509215538_AddMoreInformationToTransaction.cs @@ -0,0 +1,106 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Hutopy.Infrastructure.Migrations +{ + /// + public partial class AddMoreInformationToTransaction : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Paid", + table: "UserTransactions", + type: "bit", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "StripeBillingDetailEmail", + table: "UserTransactions", + type: "nvarchar(max)", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "StripeBillingDetailName", + table: "UserTransactions", + type: "nvarchar(max)", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "StripeChargeId", + table: "UserTransactions", + type: "nvarchar(max)", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "StripeEventId", + table: "UserTransactions", + type: "nvarchar(max)", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "StripePaymentIntent", + table: "UserTransactions", + type: "nvarchar(max)", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "StripePaymentMethod", + table: "UserTransactions", + type: "nvarchar(max)", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "StripeReceiptUrl", + table: "UserTransactions", + type: "nvarchar(max)", + nullable: false, + defaultValue: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Paid", + table: "UserTransactions"); + + migrationBuilder.DropColumn( + name: "StripeBillingDetailEmail", + table: "UserTransactions"); + + migrationBuilder.DropColumn( + name: "StripeBillingDetailName", + table: "UserTransactions"); + + migrationBuilder.DropColumn( + name: "StripeChargeId", + table: "UserTransactions"); + + migrationBuilder.DropColumn( + name: "StripeEventId", + table: "UserTransactions"); + + migrationBuilder.DropColumn( + name: "StripePaymentIntent", + table: "UserTransactions"); + + migrationBuilder.DropColumn( + name: "StripePaymentMethod", + table: "UserTransactions"); + + migrationBuilder.DropColumn( + name: "StripeReceiptUrl", + table: "UserTransactions"); + } + } +} diff --git a/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs index dedb234..cb3b55f 100644 --- a/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs @@ -102,6 +102,37 @@ namespace Hutopy.Infrastructure.Migrations b.Property("LastModifiedBy") .HasColumnType("nvarchar(max)"); + b.Property("Paid") + .HasColumnType("bit"); + + b.Property("StripeBillingDetailEmail") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StripeBillingDetailName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StripeChargeId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StripeEventId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StripePaymentIntent") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StripePaymentMethod") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StripeReceiptUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + b.Property("TipMessage") .IsRequired() .HasColumnType("nvarchar(max)"); diff --git a/src/Infrastructure/Services/UserService.cs b/src/Infrastructure/Services/UserService.cs index 12910a4..ebb5695 100644 --- a/src/Infrastructure/Services/UserService.cs +++ b/src/Infrastructure/Services/UserService.cs @@ -1,16 +1,16 @@ -using System.Text; +using System.Text; using Google.Apis.Oauth2.v2.Data; +using System.Security.Claims; using Hutopy.Domain.Interfaces; using Hutopy.Domain.Models; using Hutopy.Infrastructure.Identity; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; namespace Hutopy.Infrastructure.Services; -public class UserService(UserManager userManager) : IUserService +public class UserService(UserManager userManager, IHttpContextAccessor contextAccessor) : IUserService { - private readonly Random _random = new(DateTime.Now.Millisecond); - public async Task CreateUserAsync(string email, string userName, string firstName, string lastName, string password) { var applicationUser = new ApplicationUser @@ -47,7 +47,7 @@ public class UserService(UserManager userManager) : IUserServic UserName = response.UserName, FirstName = response.FirstName, LastName = response.LastName, - Email = response.Email + Email = response.Email, }; return userModel; @@ -56,13 +56,13 @@ public class UserService(UserManager userManager) : IUserServic public async Task GetCurrentUserAsync() { // todo: Get the id of the user doing the request. - var userId = ""; - if (string.IsNullOrEmpty(userId)) + var currentUserId = contextAccessor.HttpContext?.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value; + if (string.IsNullOrEmpty(currentUserId)) { return null; } - return await FindUserByIdAsync(userId); + return await FindUserByIdAsync(currentUserId); } public async Task FindUserByEmailAsync(string email) diff --git a/src/Infrastructure/Stripe/StripeService.cs b/src/Infrastructure/Stripe/StripeService.cs index 881b6c4..061f9e2 100644 --- a/src/Infrastructure/Stripe/StripeService.cs +++ b/src/Infrastructure/Stripe/StripeService.cs @@ -1,17 +1,23 @@ using Stripe; using Stripe.Checkout; using Hutopy.Application.Common.Interfaces; +using Microsoft.AspNetCore.Http; +using Hutopy.Application.Common.Models; +using Hutopy.Application.Stripe.Commands; namespace Hutopy.Infrastructure.Stripe; public class StripeService : IStripeService { - public StripeService() + private readonly IHttpContextAccessor _httpContextAccessor; + + public StripeService(IHttpContextAccessor httpContextAccessor) { + _httpContextAccessor = httpContextAccessor; StripeConfiguration.ApiKey = ""; } - public async Task CreateCheckoutSession(int amount, string currency = "cad") + public async Task CreateCheckoutSession(int amount, string creatorId, string currency = "cad") { var options = new SessionCreateOptions { @@ -31,7 +37,9 @@ public class StripeService : IStripeService ], Mode = "payment", UiMode = "embedded", - ReturnUrl = "https://zealous-bay-08204590f.5.azurestaticapps.net/paymentcompleted", + ReturnUrl = $"https://hutopy.ca/paymentcompleted?creatorId={creatorId}", + InvoiceCreation = new SessionInvoiceCreationOptions(){ Enabled = true}, + ClientReferenceId = creatorId }; var service = new SessionService(); @@ -39,4 +47,26 @@ public class StripeService : IStripeService return session.ClientSecret; } + + public Result ValidateTransaction(ConfirmStripeTransactionCommand request) + { + try + { + if (request.Data.Object.Status is "succeeded") + { + return new Result(true, new List()); + } + + return new Result(false, new List()); + } + catch (StripeException e) + { + Console.WriteLine("Error: {0}", e.Message); + return new Result(false, new List{e.Message}); + } + catch (Exception e) + { + return new Result(false, new List{e.Message}); + } + } } diff --git a/src/Web/Endpoints/GetMyUser.cs b/src/Web/Endpoints/GetMyUser.cs index 5fe1c79..d7c78a6 100644 --- a/src/Web/Endpoints/GetMyUser.cs +++ b/src/Web/Endpoints/GetMyUser.cs @@ -7,6 +7,7 @@ public class GetMyUser : EndpointGroupBase public override void Map(WebApplication app) { app.MapGroup(this) + .RequireAuthorization() .MapGet(GetCurrentUser); } diff --git a/src/Web/Endpoints/Stripe.cs b/src/Web/Endpoints/Stripe.cs index b944419..2eb01dc 100644 --- a/src/Web/Endpoints/Stripe.cs +++ b/src/Web/Endpoints/Stripe.cs @@ -1,4 +1,5 @@ using Hutopy.Application.Stripe.Commands; +using Hutopy.Application.Stripe.Queries; namespace Hutopy.Web.Endpoints; @@ -8,6 +9,7 @@ public class Stripe : EndpointGroupBase { app.MapGroup(this) .MapPost(ConfirmTransaction, "/confirmTransaction") + .MapGet(GetMyLastReceipt, "/getMyLastReceipt") .MapPost(CreateSessionCheckout); } @@ -16,8 +18,13 @@ public class Stripe : EndpointGroupBase return sender.Send(command); } - private static Task ConfirmTransaction(ISender sender, ConfirmStripeTransactionCommand command) + private async static Task ConfirmTransaction(ISender sender, ConfirmStripeTransactionCommand command) { - return sender.Send(command); + return await sender.Send(command); + } + + private static async Task GetMyLastReceipt(ISender sender, [AsParameters] GetMyLastReceiptQuery query) + { + return await sender.Send(query); } } diff --git a/src/Web/Endpoints/Users.cs b/src/Web/Endpoints/Users.cs index 0492fb5..1cbd880 100644 --- a/src/Web/Endpoints/Users.cs +++ b/src/Web/Endpoints/Users.cs @@ -1,4 +1,5 @@ using Hutopy.Application.Users.Commands; +using Hutopy.Application.Users.Queries.GetMinimalUser; using Hutopy.Domain.Interfaces; using Hutopy.Infrastructure.Identity; @@ -10,6 +11,7 @@ public class Users : EndpointGroupBase { app.MapGroup(this) .MapPost(CreateUser) + .MapGet(GetMinimalUser) .MapIdentityApi(); } @@ -18,4 +20,9 @@ public class Users : EndpointGroupBase await userService.CreateUserAsync(command.EmailAddress, command.UserName, command.FirstName, command.LastName, command.Password); return await sender.Send(command); } + + private static async Task GetMinimalUser(ISender sender, [AsParameters] GetMinimalUserQuery query) + { + return await sender.Send(query); + } } diff --git a/src/Web/appsettings.Development.dist b/src/Web/appsettings.Development.dist new file mode 100644 index 0000000..58d6716 --- /dev/null +++ b/src/Web/appsettings.Development.dist @@ -0,0 +1,29 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Information", + "Microsoft.AspNetCore.SpaProxy": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "Google": { + "ClientId": "", + "ClientSecret": "", + "ProjectId": "", + "AuthUri": "", + "TokenUri": "", + "AuthProviderX509CertUrl": "", + "RedirectUris": [ + "https://hutopy.ca", + "https://hutopy.com", + "http://localhost" + ], + "JavascriptOrigins": [ + "https://hutopy.ca", + "https://hutopy.com", + "http://localhost" + ] + } +} + diff --git a/src/Web/appsettings.Development.json b/src/Web/appsettings.Development.json deleted file mode 100644 index 84308c9..0000000 --- a/src/Web/appsettings.Development.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.AspNetCore.SpaProxy": "Information", - "Microsoft.Hosting.Lifetime": "Information" - } - } -} diff --git a/src/Web/wwwroot/api/specification.json b/src/Web/wwwroot/api/specification.json index fcb4d9f..b5a4c09 100644 --- a/src/Web/wwwroot/api/specification.json +++ b/src/Web/wwwroot/api/specification.json @@ -23,7 +23,12 @@ } } } - } + }, + "security": [ + { + "JWT": [] + } + ] } }, "/api/JoinUs": { @@ -131,6 +136,48 @@ } } }, + "/api/Stripe/getMyLastReceipt": { + "get": { + "tags": [ + "Stripe" + ], + "operationId": "GetMyLastReceipt", + "parameters": [ + { + "name": "Email", + "in": "query", + "required": true, + "schema": { + "type": "string", + "nullable": true + }, + "x-position": 1 + }, + { + "name": "CreatorId", + "in": "query", + "required": true, + "schema": { + "type": "string", + "nullable": true + }, + "x-position": 2 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MyLastReceiptDto" + } + } + } + } + } + } + }, "/api/Stripe": { "post": { "tags": [ @@ -194,6 +241,36 @@ } } } + }, + "get": { + "tags": [ + "Users" + ], + "operationId": "GetMinimalUser", + "parameters": [ + { + "name": "UserId", + "in": "query", + "required": true, + "schema": { + "type": "string", + "nullable": true + }, + "x-position": 1 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MinimalUserDto" + } + } + } + } + } } }, "/api/Users/register": { @@ -638,6 +715,9 @@ "lastName": { "type": "string" }, + "userName": { + "type": "string" + }, "userTransactions": { "type": "array", "items": { @@ -736,12 +816,103 @@ "type": "object", "additionalProperties": false, "properties": { - "userTransactionId": { - "type": "string", - "format": "guid" + "id": { + "type": "string" }, - "isConfirmed": { + "object": { + "type": "string" + }, + "created": { + "type": "integer", + "format": "int32" + }, + "data": { + "$ref": "#/components/schemas/Data" + }, + "request": { + "$ref": "#/components/schemas/Request" + } + } + }, + "Data": { + "type": "object", + "additionalProperties": false, + "properties": { + "object": { + "$ref": "#/components/schemas/Object" + } + } + }, + "Object": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "amount": { + "type": "integer", + "format": "int32" + }, + "billing_details": { + "$ref": "#/components/schemas/BillingDetails" + }, + "calculated_statement_descriptor": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "paid": { "type": "boolean" + }, + "payment_intent": { + "type": "string" + }, + "payment_method": { + "type": "string" + }, + "receipt_url": { + "type": "string" + }, + "status": { + "type": "string" + }, + "failure_message": { + "type": "string" + } + } + }, + "BillingDetails": { + "type": "object", + "additionalProperties": false, + "properties": { + "email": { + "type": "string" + }, + "name": { + "type": "string" + }, + "phone": { + "type": "string" + } + } + }, + "Request": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + } + } + }, + "MyLastReceiptDto": { + "type": "object", + "additionalProperties": false, + "properties": { + "receiptUrl": { + "type": "string" } } }, @@ -785,6 +956,21 @@ } } }, + "MinimalUserDto": { + "type": "object", + "additionalProperties": false, + "properties": { + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "userName": { + "type": "string" + } + } + }, "HttpValidationProblemDetails": { "allOf": [ {