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] public record SendTipRequest( Guid CreatorId, decimal Amount, string Currency, string Message, string CheckoutSuccessUrl, string CheckoutCancelledUrl); [PublicAPI] public record SendTipResponse( string Id, string Url); [PublicAPI] public class SendTipRequestValidator : Validator { public SendTipRequestValidator() { 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"); } } [PublicAPI] public class SendTipHandler( 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( SendTipRequest req, CancellationToken ct) { CreatorReference? creator = await creatorLookup.GetCreatorAsync(req.CreatorId, ct); if (creator == null) { await SendNotFoundAsync(ct); return; } if (!creator.AcceptCharges) { await SendErrorsAsync(StatusCodes.Status400BadRequest, ct); return; } Guid tipId = Guid.CreateVersion7(); TipCheckoutSession checkout = await tipProcessor.CreateCheckoutSessionAsync( tipId, creator, req.Amount, req.Currency, req.Message, req.CheckoutSuccessUrl, req.CheckoutCancelledUrl, ct); Guid userId = User.Identity?.IsAuthenticated == true ? User.GetUserId() : AnonymousUserId; 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); await SendAsync( new SendTipResponse(checkout.Id, checkout.Url), cancellation: ct); } }