diff --git a/src/Application/Common/Interfaces/IApplicationDbContext.cs b/src/Application/Common/Interfaces/IApplicationDbContext.cs index 3615321..75eabb6 100644 --- a/src/Application/Common/Interfaces/IApplicationDbContext.cs +++ b/src/Application/Common/Interfaces/IApplicationDbContext.cs @@ -5,5 +5,6 @@ namespace Hutopy.Application.Common.Interfaces; public interface IApplicationDbContext { DbSet FutureCreators { get; } + DbSet UserTransactions { get; } Task SaveChangesAsync(CancellationToken cancellationToken); } diff --git a/src/Application/Common/Interfaces/IStripeService.cs b/src/Application/Common/Interfaces/IStripeService.cs index 1231a8e..75edbe9 100644 --- a/src/Application/Common/Interfaces/IStripeService.cs +++ b/src/Application/Common/Interfaces/IStripeService.cs @@ -3,5 +3,5 @@ namespace Hutopy.Application.Common.Interfaces; public interface IStripeService { - public Task CreateCheckoutSession(int price, string currency); + public Task CreateCheckoutSession(int amount, string currency); } diff --git a/src/Application/Stripe/Commands/ConfirmStripeTransaction.cs b/src/Application/Stripe/Commands/ConfirmStripeTransaction.cs new file mode 100644 index 0000000..7aa8a3e --- /dev/null +++ b/src/Application/Stripe/Commands/ConfirmStripeTransaction.cs @@ -0,0 +1,24 @@ +using Hutopy.Application.Common.Interfaces; + +namespace Hutopy.Application.Stripe.Commands; +public record ConfirmStripeTransactionCommand : IRequest +{ + public required Guid UserTransactionId { get; init; } + public required bool IsConfirmed { get; init; } +} + +public class ConfirmStripeTransactionCommandHandler( + IApplicationDbContext dbContext + ) + : 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); + + return transaction.Id.ToString(); + } +} diff --git a/src/Application/Stripe/Commands/CreateSessionCheckoutCommand.cs b/src/Application/Stripe/Commands/CreateSessionCheckoutCommand.cs index 878a2d1..b428733 100644 --- a/src/Application/Stripe/Commands/CreateSessionCheckoutCommand.cs +++ b/src/Application/Stripe/Commands/CreateSessionCheckoutCommand.cs @@ -1,25 +1,38 @@ using Hutopy.Application.Common.Interfaces; - +using Hutopy.Domain.Entities; namespace Hutopy.Application.Stripe.Commands; -public abstract record CreateSessionCheckoutCommand : IRequest +public record CreateSessionCheckoutCommand : IRequest { - public required int Price { get; init; } - - public string Currency { get; init; } = "cad"; + public required string CreatorId { get; init; } + public required int Amount { get; init; } + public string Currency { get; init; } = "CAD"; + public string TipMessage { get; init; } = string.Empty; } - public class CreateSessionCheckoutCommandHandler( - IApplicationDbContext context, - IStripeService stripeService) + IApplicationDbContext dbContext, + IStripeService stripeService + ) : IRequestHandler { - private readonly IApplicationDbContext _context = context; - public async Task Handle(CreateSessionCheckoutCommand request, CancellationToken cancellationToken) { - var stripeSecret = await stripeService.CreateCheckoutSession(request.Price, request.Currency); + var stripeSecret = await stripeService.CreateCheckoutSession(request.Amount, 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 + }; + + await dbContext.UserTransactions.AddAsync(userTransaction, cancellationToken); + + await dbContext.SaveChangesAsync(cancellationToken); return stripeSecret; } diff --git a/src/Application/Users/Queries/GetCurrentUser.cs b/src/Application/Users/Queries/GetCurrentUser.cs new file mode 100644 index 0000000..a087cf8 --- /dev/null +++ b/src/Application/Users/Queries/GetCurrentUser.cs @@ -0,0 +1,37 @@ +using Hutopy.Application.Common.Interfaces; +using Hutopy.Domain.Interfaces; + +namespace Hutopy.Application.Users.Queries; + +public record GetCurrentUserQuery : IRequest; + +public class GetCurrentUserQueryHandler( + IApplicationDbContext context, + IMapper mapper, + IUserService userService + ) + : IRequestHandler +{ + public async Task Handle(GetCurrentUserQuery request, CancellationToken cancellationToken) + { + var identityUser = await userService.GetCurrentUserAsync(); + var currentUserId = new Guid(identityUser?.Id ?? ""); + + var transactions = await context.UserTransactions + .Where(x => x.Id == currentUserId) + .OrderBy(x => x.LastModified) + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(cancellationToken); + + + var user = new UserDto() + { + Id = currentUserId, + FirstName = identityUser?.FirstName ?? "", + LastName = identityUser?.LastName ?? "", + UserTransactions = transactions + }; + + return user; + } +} diff --git a/src/Application/Users/Queries/UserDto.cs b/src/Application/Users/Queries/UserDto.cs new file mode 100644 index 0000000..da62e26 --- /dev/null +++ b/src/Application/Users/Queries/UserDto.cs @@ -0,0 +1,12 @@ +namespace Hutopy.Application.Users.Queries; + +public class UserDto +{ + public Guid Id { get; init; } + + public required string FirstName { get; init; } + + public required string LastName { get; init; } + + public List UserTransactions { get; init; } = []; +} diff --git a/src/Application/Users/Queries/UserTransactionDto.cs b/src/Application/Users/Queries/UserTransactionDto.cs new file mode 100644 index 0000000..e6103c7 --- /dev/null +++ b/src/Application/Users/Queries/UserTransactionDto.cs @@ -0,0 +1,20 @@ +using Hutopy.Domain.Entities; + +namespace Hutopy.Application.Users.Queries; + +public class UserTransactionDto +{ + public required decimal Amount { get; init; } + + public string Currency { get; init; } = "cad"; + + public string TipMessage { get; init; } = string.Empty; + + private class Mapping : Profile + { + public Mapping() + { + CreateMap(); + } + } +} diff --git a/src/Domain/Entities/UserTransaction.cs b/src/Domain/Entities/UserTransaction.cs new file mode 100644 index 0000000..b879fd2 --- /dev/null +++ b/src/Domain/Entities/UserTransaction.cs @@ -0,0 +1,12 @@ +namespace Hutopy.Domain.Entities; + +public class UserTransaction : BaseAuditableEntity +{ + public decimal Amount { get; set; } + public string Currency { get; set; } = "CAD"; + public string TipMessage { get; set; } = string.Empty; + + // Foreign key to ApplicationUser + public string ApplicationUserId { get; set; } = string.Empty; + public bool IsConfirmed { get; set; } = false; +} diff --git a/src/Domain/Interfaces/IUserService.cs b/src/Domain/Interfaces/IUserService.cs index c59cc2a..2af0f97 100644 --- a/src/Domain/Interfaces/IUserService.cs +++ b/src/Domain/Interfaces/IUserService.cs @@ -10,6 +10,7 @@ public interface IUserService Task CreateUserAsync(Userinfo userInfo); Task FindUserByIdAsync(string id); + Task GetCurrentUserAsync(); Task FindUserByEmailAsync(string id); } diff --git a/src/Infrastructure/Data/ApplicationDbContext.cs b/src/Infrastructure/Data/ApplicationDbContext.cs index 1dd4d94..cb3a247 100644 --- a/src/Infrastructure/Data/ApplicationDbContext.cs +++ b/src/Infrastructure/Data/ApplicationDbContext.cs @@ -12,10 +12,21 @@ public class ApplicationDbContext( : IdentityDbContext(options), IApplicationDbContext { public DbSet FutureCreators => Set(); + public DbSet UserTransactions => Set(); protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); + + // Relationship between ApplicationUser and UserTransaction + builder.Entity() + .HasOne() + .WithMany() + .HasForeignKey(ut => ut.ApplicationUserId) + .IsRequired(); + + builder.Entity().Property(x => x.Amount).HasPrecision(18, 2); + builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); } } diff --git a/src/Infrastructure/DependencyInjection.cs b/src/Infrastructure/DependencyInjection.cs index e0bf63d..ae83424 100644 --- a/src/Infrastructure/DependencyInjection.cs +++ b/src/Infrastructure/DependencyInjection.cs @@ -26,7 +26,7 @@ public static class DependencyInjection var dbPassword = configuration["DB_PASSWORD"] ?? ""; var dbHost = configuration["DB_HOST"] ?? "localhost"; - if (dbHost == "localhost" && dbPassword != string.Empty) + if (dbPassword != string.Empty) { connectionString = connectionString.Replace("{DB_PASSWORD}", dbPassword); connectionString = connectionString.Replace("{DB_HOST}", dbHost); diff --git a/src/Infrastructure/Infrastructure.csproj b/src/Infrastructure/Infrastructure.csproj index 2a665da..2958bd6 100644 --- a/src/Infrastructure/Infrastructure.csproj +++ b/src/Infrastructure/Infrastructure.csproj @@ -19,7 +19,4 @@ - - - diff --git a/src/Infrastructure/Migrations/20240422173330_AddUserTransactions.Designer.cs b/src/Infrastructure/Migrations/20240422173330_AddUserTransactions.Designer.cs new file mode 100644 index 0000000..cc634ad --- /dev/null +++ b/src/Infrastructure/Migrations/20240422173330_AddUserTransactions.Designer.cs @@ -0,0 +1,383 @@ +// +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("20240422173330_AddUserTransactions")] + partial class AddUserTransactions + { + /// + 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") + .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("LastModified") + .HasColumnType("datetimeoffset"); + + b.Property("LastModifiedBy") + .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/20240422173330_AddUserTransactions.cs b/src/Infrastructure/Migrations/20240422173330_AddUserTransactions.cs new file mode 100644 index 0000000..f60e3fc --- /dev/null +++ b/src/Infrastructure/Migrations/20240422173330_AddUserTransactions.cs @@ -0,0 +1,52 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Hutopy.Infrastructure.Migrations +{ + /// + public partial class AddUserTransactions : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "UserTransactions", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Amount = table.Column(type: "decimal(18,2)", nullable: false), + Currency = table.Column(type: "nvarchar(max)", nullable: false), + TipMessage = table.Column(type: "nvarchar(max)", nullable: false), + ApplicationUserId = table.Column(type: "nvarchar(450)", nullable: false), + Created = table.Column(type: "datetimeoffset", nullable: false), + CreatedBy = table.Column(type: "nvarchar(max)", nullable: true), + LastModified = table.Column(type: "datetimeoffset", nullable: false), + LastModifiedBy = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_UserTransactions", x => x.Id); + table.ForeignKey( + name: "FK_UserTransactions_AspNetUsers_ApplicationUserId", + column: x => x.ApplicationUserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_UserTransactions_ApplicationUserId", + table: "UserTransactions", + column: "ApplicationUserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "UserTransactions"); + } + } +} diff --git a/src/Infrastructure/Migrations/20240425020920_AddIsConfirmedToUserTransaction.Designer.cs b/src/Infrastructure/Migrations/20240425020920_AddIsConfirmedToUserTransaction.Designer.cs new file mode 100644 index 0000000..80ac05b --- /dev/null +++ b/src/Infrastructure/Migrations/20240425020920_AddIsConfirmedToUserTransaction.Designer.cs @@ -0,0 +1,387 @@ +// +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("20240425020920_AddIsConfirmedToUserTransaction")] + partial class AddIsConfirmedToUserTransaction + { + /// + 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("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/20240425020920_AddIsConfirmedToUserTransaction.cs b/src/Infrastructure/Migrations/20240425020920_AddIsConfirmedToUserTransaction.cs new file mode 100644 index 0000000..39ded1c --- /dev/null +++ b/src/Infrastructure/Migrations/20240425020920_AddIsConfirmedToUserTransaction.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Hutopy.Infrastructure.Migrations +{ + /// + public partial class AddIsConfirmedToUserTransaction : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsConfirmed", + table: "UserTransactions", + type: "bit", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IsConfirmed", + table: "UserTransactions"); + } + } +} diff --git a/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs index 33d2e9d..dedb234 100644 --- a/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs @@ -69,6 +69,50 @@ namespace Hutopy.Infrastructure.Migrations 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("TipMessage") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationUserId"); + + b.ToTable("UserTransactions"); + }); + modelBuilder.Entity("Hutopy.Infrastructure.Identity.ApplicationUser", b => { b.Property("Id") @@ -275,6 +319,15 @@ namespace Hutopy.Infrastructure.Migrations 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) diff --git a/src/Infrastructure/Services/UserService.cs b/src/Infrastructure/Services/UserService.cs index 767a1c4..b45e3b3 100644 --- a/src/Infrastructure/Services/UserService.cs +++ b/src/Infrastructure/Services/UserService.cs @@ -1,5 +1,4 @@ -using System.Security.Cryptography; -using System.Text; +using System.Text; using Google.Apis.Oauth2.v2.Data; using Hutopy.Domain.Interfaces; using Hutopy.Domain.Models; @@ -21,10 +20,11 @@ public class UserService(UserManager userManager) : IUserServic FirstName = firstName, LastName = lastName }; - + + //todo: Need to handle errors better for the user. var response = await userManager.CreateAsync(applicationUser, password); - - if (!response.Succeeded) + + if (response.Errors.Any()) { throw new Exception("Failed to create user", new AggregateException(response.Errors.Select(e => new Exception(e.Description)))); } @@ -52,6 +52,18 @@ public class UserService(UserManager userManager) : IUserServic return userModel; } + + public async Task GetCurrentUserAsync() + { + // todo: Get the id of the user doing the request. + var userId = ""; + if (string.IsNullOrEmpty(userId)) + { + return null; + } + + return await FindUserByIdAsync(userId); + } public async Task FindUserByEmailAsync(string email) { diff --git a/src/Infrastructure/Stripe/StripeService.cs b/src/Infrastructure/Stripe/StripeService.cs index f54fda4..881b6c4 100644 --- a/src/Infrastructure/Stripe/StripeService.cs +++ b/src/Infrastructure/Stripe/StripeService.cs @@ -6,13 +6,12 @@ namespace Hutopy.Infrastructure.Stripe; public class StripeService : IStripeService { - public StripeService() - { - // I removed the key to push. Will need to be in config. - StripeConfiguration.ApiKey = ""; - } + public StripeService() + { + StripeConfiguration.ApiKey = ""; + } - public async Task CreateCheckoutSession(int price, string currency = "cad") + public async Task CreateCheckoutSession(int amount, string currency = "cad") { var options = new SessionCreateOptions { @@ -22,7 +21,7 @@ public class StripeService : IStripeService { PriceData = new SessionLineItemPriceDataOptions { - UnitAmount = price, + UnitAmount = amount, Currency = currency, ProductData = new SessionLineItemPriceDataProductDataOptions { Name = "Tip", }, }, @@ -32,7 +31,7 @@ public class StripeService : IStripeService ], Mode = "payment", UiMode = "embedded", - ReturnUrl = $"http://localhost:5173/creatorfolio", + ReturnUrl = "https://zealous-bay-08204590f.5.azurestaticapps.net/paymentcompleted", }; var service = new SessionService(); diff --git a/src/Web/Endpoints/GetMyUser.cs b/src/Web/Endpoints/GetMyUser.cs new file mode 100644 index 0000000..5fe1c79 --- /dev/null +++ b/src/Web/Endpoints/GetMyUser.cs @@ -0,0 +1,17 @@ +using Hutopy.Application.Users.Queries; + +namespace Hutopy.Web.Endpoints; + +public class GetMyUser : EndpointGroupBase +{ + public override void Map(WebApplication app) + { + app.MapGroup(this) + .MapGet(GetCurrentUser); + } + + private static async Task GetCurrentUser(ISender sender, [AsParameters] GetCurrentUserQuery query) + { + return await sender.Send(query); + } +} diff --git a/src/Web/Endpoints/Stripe.cs b/src/Web/Endpoints/Stripe.cs index b5c4a6a..b944419 100644 --- a/src/Web/Endpoints/Stripe.cs +++ b/src/Web/Endpoints/Stripe.cs @@ -7,6 +7,7 @@ public class Stripe : EndpointGroupBase public override void Map(WebApplication app) { app.MapGroup(this) + .MapPost(ConfirmTransaction, "/confirmTransaction") .MapPost(CreateSessionCheckout); } @@ -14,4 +15,9 @@ public class Stripe : EndpointGroupBase { return sender.Send(command); } + + private static Task ConfirmTransaction(ISender sender, ConfirmStripeTransactionCommand command) + { + return sender.Send(command); + } } diff --git a/src/Web/Endpoints/Users.cs b/src/Web/Endpoints/Users.cs index 38ccb1e..0492fb5 100644 --- a/src/Web/Endpoints/Users.cs +++ b/src/Web/Endpoints/Users.cs @@ -13,7 +13,7 @@ public class Users : EndpointGroupBase .MapIdentityApi(); } - public async Task CreateUser(ISender sender, CreateUserCommand command, IUserService userService) + private static async Task CreateUser(ISender sender, CreateUserCommand command, IUserService userService) { await userService.CreateUserAsync(command.EmailAddress, command.UserName, command.FirstName, command.LastName, command.Password); return await sender.Send(command); diff --git a/src/Web/wwwroot/api/specification.json b/src/Web/wwwroot/api/specification.json index fc53d77..5a1838c 100644 --- a/src/Web/wwwroot/api/specification.json +++ b/src/Web/wwwroot/api/specification.json @@ -6,32 +6,19 @@ "version": "1.0.0" }, "paths": { - "/api/Google": { - "post": { + "/api/GetMyUser": { + "get": { "tags": [ - "Google" + "GetMyUser" ], - "operationId": "CreateGoogleUser", - "requestBody": { - "x-name": "command", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateGoogleUserCommand" - } - } - }, - "required": true, - "x-position": 1 - }, + "operationId": "GetCurrentUser", "responses": { "200": { "description": "", "content": { "application/json": { "schema": { - "type": "string", - "format": "guid" + "$ref": "#/components/schemas/UserDto" } } } @@ -112,6 +99,38 @@ } } }, + "/api/Stripe/confirmTransaction": { + "post": { + "tags": [ + "Stripe" + ], + "operationId": "ConfirmTransaction", + "requestBody": { + "x-name": "command", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConfirmStripeTransactionCommand" + } + } + }, + "required": true, + "x-position": 1 + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, "/api/Stripe": { "post": { "tags": [ @@ -592,11 +611,40 @@ }, "components": { "schemas": { - "CreateGoogleUserCommand": { + "UserDto": { "type": "object", "additionalProperties": false, "properties": { - "accessToken": { + "id": { + "type": "string", + "format": "guid" + }, + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "userTransactions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserTransactionDto" + } + } + } + }, + "UserTransactionDto": { + "type": "object", + "additionalProperties": false, + "properties": { + "amount": { + "type": "number", + "format": "decimal" + }, + "currency": { + "type": "string" + }, + "tipMessage": { "type": "string" } } @@ -671,17 +719,35 @@ } } }, - "CreateSessionCheckoutCommand": { + "ConfirmStripeTransactionCommand": { "type": "object", - "x-abstract": true, "additionalProperties": false, "properties": { - "price": { + "userTransactionId": { + "type": "string", + "format": "guid" + }, + "isConfirmed": { + "type": "boolean" + } + } + }, + "CreateSessionCheckoutCommand": { + "type": "object", + "additionalProperties": false, + "properties": { + "creatorId": { + "type": "string" + }, + "amount": { "type": "integer", "format": "int32" }, "currency": { "type": "string" + }, + "tipMessage": { + "type": "string" } } },