many fixes and improvements - rework for modules/ and common/

feat(emailer): add Postmark and Resend providers
This commit is contained in:
2025-06-06 12:21:43 -04:00
parent 31ba18fa8d
commit 25b94d3e02
313 changed files with 6586 additions and 18260 deletions

View File

@@ -0,0 +1,49 @@
using Hutopy.Infrastructure.Security;
using Hutopy.Modules.Identity.Contracts;
using Hutopy.Modules.Tipping.Data;
using Hutopy.Modules.Tipping.Models;
namespace Hutopy.Modules.Tipping.Handlers;
[PublicAPI]
public record struct GetReceivedTipsResponse(
IEnumerable<TipReceivedModel> Tips);
[PublicAPI]
public class GetReceivedTipsHandler(
IUserLookup userLookup,
TippingDbContext dbContext)
: EndpointWithoutRequest<GetReceivedTipsResponse>
{
public override void Configure()
{
Get("/api/tips");
Options(o => o.WithTags("Tips"));
}
public override async Task HandleAsync(
CancellationToken ct)
{
var tips = await dbContext
.Tips
.Where(tip => tip.CreatorId == User.GetUserId())
.ToListAsync(ct);
var result = await Task.WhenAll(
tips.Select(async tip =>
{
var tipper = await userLookup.GetUserAsync(tip.CreatorId, ct);
return new TipReceivedModel(
tip.Id,
tip.CreatedAt,
tip.CreatedBy,
tipper?.Fullname ?? "Unknown User",
tip.Amount,
tip.Currency,
tip.Message);
}));
await SendOkAsync(new GetReceivedTipsResponse(result), ct);
}
}

View File

@@ -0,0 +1,116 @@
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<SendTipRequest>
{
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<SendTipRequest, SendTipResponse>
{
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);
}
}