From b63d53f109ddbf431309b2818ebf6e166bd0102d Mon Sep 17 00:00:00 2001 From: Dominic Villemure Date: Mon, 22 Apr 2024 15:55:49 -0400 Subject: [PATCH 1/5] #27 added userTransactions --- .../Interfaces/IApplicationDbContext.cs | 1 + .../Commands/CreateSessionCheckoutCommand.cs | 24 +- .../Users/Queries/GetCurrentUser.cs | 37 ++ src/Application/Users/Queries/UserDto.cs | 12 + .../Users/Queries/UserTransactionDto.cs | 20 + src/Domain/Entities/UserTransaction.cs | 11 + src/Domain/Interfaces/IUserService.cs | 1 + .../Data/ApplicationDbContext.cs | 9 + src/Infrastructure/Infrastructure.csproj | 3 - ...0422173330_AddUserTransactions.Designer.cs | 383 ++++++++++++++++++ .../20240422173330_AddUserTransactions.cs | 52 +++ .../ApplicationDbContextModelSnapshot.cs | 49 +++ src/Infrastructure/Services/UserService.cs | 26 +- src/Infrastructure/Stripe/StripeService.cs | 12 +- src/Web/Endpoints/GetMyUser.cs | 17 + src/Web/Endpoints/Users.cs | 2 +- src/Web/wwwroot/api/specification.json | 65 ++- 17 files changed, 696 insertions(+), 28 deletions(-) create mode 100644 src/Application/Users/Queries/GetCurrentUser.cs create mode 100644 src/Application/Users/Queries/UserDto.cs create mode 100644 src/Application/Users/Queries/UserTransactionDto.cs create mode 100644 src/Domain/Entities/UserTransaction.cs create mode 100644 src/Infrastructure/Migrations/20240422173330_AddUserTransactions.Designer.cs create mode 100644 src/Infrastructure/Migrations/20240422173330_AddUserTransactions.cs create mode 100644 src/Web/Endpoints/GetMyUser.cs 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/Stripe/Commands/CreateSessionCheckoutCommand.cs b/src/Application/Stripe/Commands/CreateSessionCheckoutCommand.cs index 878a2d1..e627bf1 100644 --- a/src/Application/Stripe/Commands/CreateSessionCheckoutCommand.cs +++ b/src/Application/Stripe/Commands/CreateSessionCheckoutCommand.cs @@ -1,25 +1,31 @@ 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 string CreatorId { get; init; } public required int Price { get; init; } - - public string Currency { get; init; } = "cad"; + 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 userTransaction = new UserTransaction + { + Currency = request.Currency, Amount = request.Price, TipMessage = request.TipMessage, ApplicationUserId = request.CreatorId + }; + + await dbContext.UserTransactions.AddAsync(userTransaction, 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..4617c46 --- /dev/null +++ b/src/Domain/Entities/UserTransaction.cs @@ -0,0 +1,11 @@ +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; +} diff --git a/src/Domain/Interfaces/IUserService.cs b/src/Domain/Interfaces/IUserService.cs index c3bd5ac..efe46bb 100644 --- a/src/Domain/Interfaces/IUserService.cs +++ b/src/Domain/Interfaces/IUserService.cs @@ -7,6 +7,7 @@ public interface IUserService Task CreateUserAsync(string email, string userName, string firstName, string lastName, string password); 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..8ae973e 100644 --- a/src/Infrastructure/Data/ApplicationDbContext.cs +++ b/src/Infrastructure/Data/ApplicationDbContext.cs @@ -12,10 +12,19 @@ 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.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); } } 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/ApplicationDbContextModelSnapshot.cs b/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs index 33d2e9d..157cea1 100644 --- a/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs @@ -69,6 +69,46 @@ namespace Hutopy.Infrastructure.Migrations 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") @@ -275,6 +315,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 98c0c7c..7fae66c 100644 --- a/src/Infrastructure/Services/UserService.cs +++ b/src/Infrastructure/Services/UserService.cs @@ -1,15 +1,14 @@ -using Hutopy.Domain.Interfaces; +using System.Security.Claims; +using Hutopy.Domain.Interfaces; using Hutopy.Domain.Models; using Hutopy.Infrastructure.Identity; -using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; namespace Hutopy.Infrastructure.Services; -public class UserService(UserManager userManager) : IUserService +public class UserService(UserManager userManager, IHttpContextAccessor httpContextAccessor) : IUserService { - private readonly UserManager _userManager = userManager; - public async Task CreateUserAsync(string email, string userName, string firstName, string lastName, string password) { var applicationUser = new ApplicationUser @@ -21,7 +20,7 @@ public class UserService(UserManager userManager) : IUserServic }; //todo: Need to handle errors better for the user. - var response = await _userManager.CreateAsync(applicationUser, password); + var response = await userManager.CreateAsync(applicationUser, password); if (response.Errors.Any()) { @@ -31,7 +30,7 @@ public class UserService(UserManager userManager) : IUserServic public async Task FindUserByIdAsync(string id) { - var response = await _userManager.FindByIdAsync(id); + var response = await userManager.FindByIdAsync(id); if (response == null) return null; @@ -46,10 +45,21 @@ public class UserService(UserManager userManager) : IUserServic return userModel; } + + public async Task GetCurrentUserAsync() + { + var userId = httpContextAccessor.HttpContext?.User.FindFirstValue(ClaimTypes.NameIdentifier); + if (string.IsNullOrEmpty(userId)) + { + return null; + } + + return await FindUserByIdAsync(userId); + } public async Task FindUserByEmailAsync(string email) { - var response = await _userManager.FindByEmailAsync(email); + var response = await userManager.FindByEmailAsync(email); if (response == null) return null; diff --git a/src/Infrastructure/Stripe/StripeService.cs b/src/Infrastructure/Stripe/StripeService.cs index f54fda4..154c541 100644 --- a/src/Infrastructure/Stripe/StripeService.cs +++ b/src/Infrastructure/Stripe/StripeService.cs @@ -1,16 +1,16 @@ using Stripe; using Stripe.Checkout; using Hutopy.Application.Common.Interfaces; +using Microsoft.Extensions.Configuration; 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(IConfiguration configuration) + { + StripeConfiguration.ApiKey = configuration["STRIPE_API_KEY"]; + } public async Task CreateCheckoutSession(int price, string currency = "cad") { @@ -32,7 +32,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/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 944608b..5e5b6e3 100644 --- a/src/Web/wwwroot/api/specification.json +++ b/src/Web/wwwroot/api/specification.json @@ -6,6 +6,26 @@ "version": "1.0.0" }, "paths": { + "/api/GetMyUser": { + "get": { + "tags": [ + "GetMyUser" + ], + "operationId": "GetCurrentUser", + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserDto" + } + } + } + } + } + } + }, "/api/JoinUs": { "get": { "tags": [ @@ -559,6 +579,44 @@ }, "components": { "schemas": { + "UserDto": { + "type": "object", + "additionalProperties": false, + "properties": { + "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" + } + } + }, "PaginatedListOfFutureCreatorListDto": { "type": "object", "additionalProperties": false, @@ -631,15 +689,20 @@ }, "CreateSessionCheckoutCommand": { "type": "object", - "x-abstract": true, "additionalProperties": false, "properties": { + "creatorId": { + "type": "string" + }, "price": { "type": "integer", "format": "int32" }, "currency": { "type": "string" + }, + "tipMessage": { + "type": "string" } } }, From bd6028c524913d9ca988efdc9c8a0e72b557711e Mon Sep 17 00:00:00 2001 From: Dominic Villemure Date: Mon, 22 Apr 2024 16:34:17 -0400 Subject: [PATCH 2/5] #27 saveChanges --- .../Stripe/Commands/CreateSessionCheckoutCommand.cs | 2 ++ src/Infrastructure/Services/UserService.cs | 9 ++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Application/Stripe/Commands/CreateSessionCheckoutCommand.cs b/src/Application/Stripe/Commands/CreateSessionCheckoutCommand.cs index e627bf1..9014f97 100644 --- a/src/Application/Stripe/Commands/CreateSessionCheckoutCommand.cs +++ b/src/Application/Stripe/Commands/CreateSessionCheckoutCommand.cs @@ -27,6 +27,8 @@ public class CreateSessionCheckoutCommandHandler( await dbContext.UserTransactions.AddAsync(userTransaction, cancellationToken); + await dbContext.SaveChangesAsync(cancellationToken); + return stripeSecret; } } diff --git a/src/Infrastructure/Services/UserService.cs b/src/Infrastructure/Services/UserService.cs index 7fae66c..b0704c2 100644 --- a/src/Infrastructure/Services/UserService.cs +++ b/src/Infrastructure/Services/UserService.cs @@ -1,13 +1,11 @@ -using System.Security.Claims; -using Hutopy.Domain.Interfaces; +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, IHttpContextAccessor httpContextAccessor) : IUserService +public class UserService(UserManager userManager) : IUserService { public async Task CreateUserAsync(string email, string userName, string firstName, string lastName, string password) { @@ -48,7 +46,8 @@ public class UserService(UserManager userManager, IHttpContextA public async Task GetCurrentUserAsync() { - var userId = httpContextAccessor.HttpContext?.User.FindFirstValue(ClaimTypes.NameIdentifier); + // todo: Get the id of the user doing the request. + var userId = ""; if (string.IsNullOrEmpty(userId)) { return null; From 6883855ecbd491f29f233839593a0e7adb9e5a3b Mon Sep 17 00:00:00 2001 From: Dominic Villemure Date: Wed, 24 Apr 2024 19:37:35 -0400 Subject: [PATCH 3/5] #27 small changes for the userTransaction --- src/Application/Common/Interfaces/IStripeService.cs | 2 +- .../Stripe/Commands/CreateSessionCheckoutCommand.cs | 11 ++++++++--- src/Infrastructure/Stripe/StripeService.cs | 11 +++++------ src/Web/wwwroot/api/specification.json | 2 +- 4 files changed, 15 insertions(+), 11 deletions(-) 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/CreateSessionCheckoutCommand.cs b/src/Application/Stripe/Commands/CreateSessionCheckoutCommand.cs index 9014f97..b428733 100644 --- a/src/Application/Stripe/Commands/CreateSessionCheckoutCommand.cs +++ b/src/Application/Stripe/Commands/CreateSessionCheckoutCommand.cs @@ -5,7 +5,7 @@ namespace Hutopy.Application.Stripe.Commands; public record CreateSessionCheckoutCommand : IRequest { public required string CreatorId { get; init; } - public required int Price { get; init; } + public required int Amount { get; init; } public string Currency { get; init; } = "CAD"; public string TipMessage { get; init; } = string.Empty; } @@ -18,11 +18,16 @@ public class CreateSessionCheckoutCommandHandler( { 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 = request.Price, TipMessage = request.TipMessage, ApplicationUserId = request.CreatorId + Currency = request.Currency, Amount = priceInDollars, TipMessage = request.TipMessage, ApplicationUserId = request.CreatorId }; await dbContext.UserTransactions.AddAsync(userTransaction, cancellationToken); diff --git a/src/Infrastructure/Stripe/StripeService.cs b/src/Infrastructure/Stripe/StripeService.cs index 154c541..881b6c4 100644 --- a/src/Infrastructure/Stripe/StripeService.cs +++ b/src/Infrastructure/Stripe/StripeService.cs @@ -1,18 +1,17 @@ using Stripe; using Stripe.Checkout; using Hutopy.Application.Common.Interfaces; -using Microsoft.Extensions.Configuration; namespace Hutopy.Infrastructure.Stripe; public class StripeService : IStripeService { - public StripeService(IConfiguration configuration) + public StripeService() { - StripeConfiguration.ApiKey = configuration["STRIPE_API_KEY"]; + 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 = $"https://zealous-bay-08204590f.5.azurestaticapps.net/paymentcompleted", + ReturnUrl = "https://zealous-bay-08204590f.5.azurestaticapps.net/paymentcompleted", }; var service = new SessionService(); diff --git a/src/Web/wwwroot/api/specification.json b/src/Web/wwwroot/api/specification.json index 5e5b6e3..1bdd8f1 100644 --- a/src/Web/wwwroot/api/specification.json +++ b/src/Web/wwwroot/api/specification.json @@ -694,7 +694,7 @@ "creatorId": { "type": "string" }, - "price": { + "amount": { "type": "integer", "format": "int32" }, From cba08b04642d8611eab055958b8493a891d44622 Mon Sep 17 00:00:00 2001 From: Dominic Villemure Date: Wed, 24 Apr 2024 22:14:14 -0400 Subject: [PATCH 4/5] #27 added confirm endpoint for stripe --- .../Commands/ConfirmStripeTransaction.cs | 24 ++ src/Domain/Entities/UserTransaction.cs | 1 + .../Data/ApplicationDbContext.cs | 2 + ...ddIsConfirmedToUserTransaction.Designer.cs | 387 ++++++++++++++++++ ...5020920_AddIsConfirmedToUserTransaction.cs | 29 ++ .../ApplicationDbContextModelSnapshot.cs | 4 + src/Web/Endpoints/Stripe.cs | 6 + src/Web/wwwroot/api/specification.json | 45 ++ 8 files changed, 498 insertions(+) create mode 100644 src/Application/Stripe/Commands/ConfirmStripeTransaction.cs create mode 100644 src/Infrastructure/Migrations/20240425020920_AddIsConfirmedToUserTransaction.Designer.cs create mode 100644 src/Infrastructure/Migrations/20240425020920_AddIsConfirmedToUserTransaction.cs 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/Domain/Entities/UserTransaction.cs b/src/Domain/Entities/UserTransaction.cs index 4617c46..b879fd2 100644 --- a/src/Domain/Entities/UserTransaction.cs +++ b/src/Domain/Entities/UserTransaction.cs @@ -8,4 +8,5 @@ public class UserTransaction : BaseAuditableEntity // Foreign key to ApplicationUser public string ApplicationUserId { get; set; } = string.Empty; + public bool IsConfirmed { get; set; } = false; } diff --git a/src/Infrastructure/Data/ApplicationDbContext.cs b/src/Infrastructure/Data/ApplicationDbContext.cs index 8ae973e..cb3a247 100644 --- a/src/Infrastructure/Data/ApplicationDbContext.cs +++ b/src/Infrastructure/Data/ApplicationDbContext.cs @@ -24,6 +24,8 @@ public class ApplicationDbContext( .WithMany() .HasForeignKey(ut => ut.ApplicationUserId) .IsRequired(); + + builder.Entity().Property(x => x.Amount).HasPrecision(18, 2); builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); } 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 157cea1..dedb234 100644 --- a/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs @@ -76,6 +76,7 @@ namespace Hutopy.Infrastructure.Migrations .HasColumnType("uniqueidentifier"); b.Property("Amount") + .HasPrecision(18, 2) .HasColumnType("decimal(18,2)"); b.Property("ApplicationUserId") @@ -92,6 +93,9 @@ namespace Hutopy.Infrastructure.Migrations .IsRequired() .HasColumnType("nvarchar(max)"); + b.Property("IsConfirmed") + .HasColumnType("bit"); + b.Property("LastModified") .HasColumnType("datetimeoffset"); 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/wwwroot/api/specification.json b/src/Web/wwwroot/api/specification.json index 1bdd8f1..5a1838c 100644 --- a/src/Web/wwwroot/api/specification.json +++ b/src/Web/wwwroot/api/specification.json @@ -99,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": [ @@ -687,6 +719,19 @@ } } }, + "ConfirmStripeTransactionCommand": { + "type": "object", + "additionalProperties": false, + "properties": { + "userTransactionId": { + "type": "string", + "format": "guid" + }, + "isConfirmed": { + "type": "boolean" + } + } + }, "CreateSessionCheckoutCommand": { "type": "object", "additionalProperties": false, From 5980eb7f0f542d2d3310583c3356769076fd89e4 Mon Sep 17 00:00:00 2001 From: Kamigen <46357922+Edouard127@users.noreply.github.com> Date: Sun, 28 Apr 2024 19:16:40 -0400 Subject: [PATCH 5/5] Fix: String not edited if host is local --- src/Infrastructure/DependencyInjection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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);