diff --git a/src/Application/Common/Interfaces/IStripeService.cs b/src/Application/Common/Interfaces/IStripeService.cs index ca32184..6737a3d 100644 --- a/src/Application/Common/Interfaces/IStripeService.cs +++ b/src/Application/Common/Interfaces/IStripeService.cs @@ -6,6 +6,6 @@ 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 eb074a0..0ddd17e 100644 --- a/src/Application/Stripe/Commands/ConfirmStripeTransaction.cs +++ b/src/Application/Stripe/Commands/ConfirmStripeTransaction.cs @@ -56,15 +56,15 @@ public class ConfirmStripeTransactionCommandHandler( 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; } + 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); diff --git a/src/Application/Stripe/Commands/CreateSessionCheckoutCommand.cs b/src/Application/Stripe/Commands/CreateSessionCheckoutCommand.cs index 28e9d72..681c4d8 100644 --- a/src/Application/Stripe/Commands/CreateSessionCheckoutCommand.cs +++ b/src/Application/Stripe/Commands/CreateSessionCheckoutCommand.cs @@ -18,7 +18,7 @@ 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); 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 5ab7314..78e32d4 100644 --- a/src/Domain/Entities/UserTransaction.cs +++ b/src/Domain/Entities/UserTransaction.cs @@ -7,7 +7,7 @@ public class UserTransaction : BaseAuditableEntity public string TipMessage { get; set; } = string.Empty; // Foreign key to ApplicationUser - public string ApplicationUserId { get; set; } = string.Empty; + 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; diff --git a/src/Infrastructure/Services/UserService.cs b/src/Infrastructure/Services/UserService.cs index b0704c2..66dca70 100644 --- a/src/Infrastructure/Services/UserService.cs +++ b/src/Infrastructure/Services/UserService.cs @@ -1,11 +1,13 @@ -using Hutopy.Domain.Interfaces; +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 { public async Task CreateUserAsync(string email, string userName, string firstName, string lastName, string password) { @@ -38,7 +40,7 @@ public class UserService(UserManager userManager) : IUserServic UserName = response.UserName, FirstName = response.FirstName, LastName = response.LastName, - Email = response.Email + Email = response.Email, }; return userModel; @@ -47,13 +49,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 dc84e34..061f9e2 100644 --- a/src/Infrastructure/Stripe/StripeService.cs +++ b/src/Infrastructure/Stripe/StripeService.cs @@ -9,7 +9,6 @@ namespace Hutopy.Infrastructure.Stripe; public class StripeService : IStripeService { - const string EndpointSecret = ""; private readonly IHttpContextAccessor _httpContextAccessor; public StripeService(IHttpContextAccessor httpContextAccessor) @@ -18,7 +17,7 @@ public class StripeService : IStripeService 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 { @@ -38,7 +37,9 @@ public class StripeService : IStripeService ], Mode = "payment", UiMode = "embedded", - ReturnUrl = "https://hutopy.ca/paymentcompleted", + ReturnUrl = $"https://hutopy.ca/paymentcompleted?creatorId={creatorId}", + InvoiceCreation = new SessionInvoiceCreationOptions(){ Enabled = true}, + ClientReferenceId = creatorId }; var service = new SessionService(); @@ -57,7 +58,6 @@ public class StripeService : IStripeService } return new Result(false, new List()); - } catch (StripeException e) { 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 264f4c8..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); } @@ -20,4 +22,9 @@ public class Stripe : EndpointGroupBase { 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/wwwroot/api/specification.json b/src/Web/wwwroot/api/specification.json index 9c7a80c..d0680de 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": { @@ -625,6 +702,9 @@ "lastName": { "type": "string" }, + "userName": { + "type": "string" + }, "userTransactions": { "type": "array", "items": { @@ -814,6 +894,15 @@ } } }, + "MyLastReceiptDto": { + "type": "object", + "additionalProperties": false, + "properties": { + "receiptUrl": { + "type": "string" + } + } + }, "CreateSessionCheckoutCommand": { "type": "object", "additionalProperties": false, @@ -854,6 +943,21 @@ } } }, + "MinimalUserDto": { + "type": "object", + "additionalProperties": false, + "properties": { + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "userName": { + "type": "string" + } + } + }, "HttpValidationProblemDetails": { "allOf": [ {