using Hutopy.Infrastructure.Security; using Hutopy.Modules.Creators.Contracts; using Hutopy.Modules.Tipping.Contracts; using Hutopy.Modules.Tipping.Data; namespace Hutopy.Modules.Tipping.Handlers; [PublicAPI] internal static class SendTip { internal record Request( Guid CreatorId, decimal Amount, string Currency, string Message, Uri CheckoutSuccessUrl, Uri CheckoutCancelledUrl); internal record Response( string Id, Uri Url); internal class Validator : Validator { public Validator() { RuleFor(x => x.Amount) .GreaterThan(0) .WithMessage("Tip amount must be greater than 0"); RuleFor(x => x.CreatorId) .NotEmpty() .WithMessage("Creator ID is required"); RuleFor(x => x.CheckoutSuccessUrl) .NotEmpty() .WithMessage("CheckoutSuccessUrl is required"); RuleFor(x => x.CheckoutCancelledUrl) .NotEmpty() .WithMessage("CheckoutCancelledUrl is required"); } } internal class Handler( TippingDbContext dbContext, ITipProcessor tipProcessor, ICreatorLookup creatorLookup) : Endpoint { private static readonly Guid AnonymousUserId = Guid.Parse("AAAAAAAA-0000-0000-0000-000000000000"); public override void Configure() { Post("/api/tips"); Options(o => o.WithTags("Memberships")); AllowAnonymous(); } public override async Task HandleAsync( Request req, CancellationToken ct) { ArgumentNullException.ThrowIfNull(req); var userId = User.Identity?.IsAuthenticated == true ? User.GetUserId() : AnonymousUserId; var creator = await creatorLookup .GetCreatorAsync(req.CreatorId, ct) .ConfigureAwait(false); if (creator == null) { await SendNotFoundAsync(ct).ConfigureAwait(false); return; } if (!creator.AcceptCharges) { await SendErrorsAsync(StatusCodes.Status400BadRequest, ct).ConfigureAwait(false); return; } var tipId = Guid.CreateVersion7(); var checkout = await tipProcessor .CreateCheckoutSessionAsync( tipId, creator, req.Amount, req.Currency, req.Message, req.CheckoutSuccessUrl, req.CheckoutCancelledUrl, ct) .ConfigureAwait(false); Tip tip = new() { Id = tipId, CreatedAt = DateTimeOffset.UtcNow, StripeSessionId = checkout.Id, CreatedBy = userId, CreatorId = req.CreatorId, Status = TipStatus.Pending, Amount = req.Amount, Currency = req.Currency, Message = req.Message }; dbContext.Tips.Add(tip); await dbContext.SaveChangesAsync(ct).ConfigureAwait(false); await SendAsync( new Response(checkout.Id, new Uri(checkout.Url)), cancellation: ct) .ConfigureAwait(false); } } }