diff --git a/EliteAPI.Tests/Guides/GuideEndpointTests.cs b/EliteAPI.Tests/Guides/GuideEndpointTests.cs index cd5c34f7..fce066db 100644 --- a/EliteAPI.Tests/Guides/GuideEndpointTests.cs +++ b/EliteAPI.Tests/Guides/GuideEndpointTests.cs @@ -363,7 +363,7 @@ await App.ModeratorClient.POSTAsync( [Fact, Priority(22)] public async Task UpdatePublishedGuide_MaintainsVisibility() { - // 1. Create and publish a guide + // Create and publish a guide var (createRsp, created) = await App.RegularUserClient.POSTAsync( new CreateGuideRequest { Type = GuideType.General }); createRsp.IsSuccessStatusCode.ShouldBeTrue(); @@ -383,17 +383,17 @@ await App.RegularUserClient.PUTAsync( MarkdownContent = "# Updated" }); - // 3. Author submits update (Status: Published -> PendingApproval) + // Author submits update (Status: Published -> PendingApproval) var submitRsp = await App.RegularUserClient.POSTAsync( new SubmitGuideRequest { GuideId = created.Id }); submitRsp.IsSuccessStatusCode.ShouldBeTrue(); - // 4. Verify Admin sees it in Pending list + // Verify Admin sees it in Pending list var (adminListRsp, pendingGuides) = await App.ModeratorClient.GETAsync>(); adminListRsp.IsSuccessStatusCode.ShouldBeTrue(); pendingGuides.ShouldContain(g => g.Id == created.Id); - // 5. Verify Public still sees the OLD version (Title should NOT be "Updated Title Pending") + // Verify Public still sees the OLD version (Title should NOT be "Updated Title Pending") var (publicListRsp, publicGuides) = await App.AnonymousClient.GETAsync>( new ListGuidesRequest()); publicListRsp.IsSuccessStatusCode.ShouldBeTrue(); @@ -401,16 +401,42 @@ await App.RegularUserClient.PUTAsync( publicGuide.ShouldNotBeNull(); publicGuide.Title.ShouldNotBe("Updated Title Pending"); // Helper likely doesn't verify exact content, but existence is key - // 6. Admin Approves + // Admin Approves await App.ModeratorClient.POSTAsync( new ApproveGuideRequest { GuideId = created.Id }); - // 7. Verify Public sees NEW version + // Verify Public sees NEW version var (finalListRsp, finalGuides) = await App.AnonymousClient.GETAsync>( new ListGuidesRequest()); finalGuides!.First(g => g.Id == created.Id).Title.ShouldBe("Updated Title Pending"); } + [Fact, Priority(23)] + public async Task UpdateGuide_Published_AsAuthor_Succeeds() + { + // Create and publish a guide + var (createRsp, created) = await App.RegularUserClient.POSTAsync( + new CreateGuideRequest { Type = GuideType.General }); + createRsp.IsSuccessStatusCode.ShouldBeTrue(); + + await App.RegularUserClient.POSTAsync( + new SubmitGuideRequest { GuideId = created!.Id }); + await App.ModeratorClient.POSTAsync( + new ApproveGuideRequest { GuideId = created.Id }); + + // Author tries to update - should succeed + var updateRsp = await App.RegularUserClient.PUTAsync( + new UpdateGuideRequest + { + Id = created.Id, + Title = "New Draft Title", + Description = "New Draft Desc", + MarkdownContent = "# New Draft Content" + }); + + updateRsp.IsSuccessStatusCode.ShouldBeTrue(); + } + protected override async ValueTask TearDownAsync() { await App.CleanUpGuidesAsync(); diff --git a/EliteAPI/Features/Guides/Endpoints/CreateCommentEndpoint.cs b/EliteAPI/Features/Guides/Endpoints/CreateCommentEndpoint.cs index 748f3d20..52a0038d 100644 --- a/EliteAPI/Features/Guides/Endpoints/CreateCommentEndpoint.cs +++ b/EliteAPI/Features/Guides/Endpoints/CreateCommentEndpoint.cs @@ -1,6 +1,8 @@ using EliteAPI.Features.Guides.Services; +using EliteAPI.Features.AuditLogs.Services; using EliteAPI.Features.Comments.Mappers; using EliteAPI.Features.Comments.Models.Dtos; +using EliteAPI.Features.Common.Services; using EliteAPI.Utilities; using FastEndpoints; using FluentValidation; @@ -9,7 +11,8 @@ namespace EliteAPI.Features.Guides.Endpoints; public class CreateCommentEndpoint( CommentService commentService, - CommentMapper mapper) : Endpoint + CommentMapper mapper, + AuditLogService auditLogService) : Endpoint { public override void Configure() { @@ -34,6 +37,14 @@ public override async Task HandleAsync(CreateCommentRequest req, CancellationTok { var comment = await commentService.AddCommentAsync(req.GuideId, EliteAPI.Features.Comments.Models.CommentTargetType.Guide, userId.Value, req.Content, req.ParentId, req.LiftedElementId); + var guideSlug = SqidService.Encode(req.GuideId); + await auditLogService.LogAsync( + userId.Value, + "comment_submitted", + "Guide", + guideSlug, + $"Commented on guide {guideSlug}"); + await Send.OkAsync(mapper.ToDto(comment, userId.Value, User.IsSupportOrHigher(), null), ct); } catch (UnauthorizedAccessException ex) diff --git a/EliteAPI/Features/Guides/Endpoints/GuideApprovalEndpoints.cs b/EliteAPI/Features/Guides/Endpoints/GuideApprovalEndpoints.cs index 532c74e3..cf5e8ae3 100644 --- a/EliteAPI/Features/Guides/Endpoints/GuideApprovalEndpoints.cs +++ b/EliteAPI/Features/Guides/Endpoints/GuideApprovalEndpoints.cs @@ -11,7 +11,11 @@ namespace EliteAPI.Features.Guides.Endpoints; /// /// Submit a guide for approval (author only) /// -public class SubmitGuideForApprovalEndpoint(GuideService guideService, UserManager userManager) : Endpoint +public class SubmitGuideForApprovalEndpoint( + GuideService guideService, + UserManager userManager, + AuditLogService auditLogService, + NotificationService notificationService) : Endpoint { public override void Configure() { @@ -59,6 +63,23 @@ public override async Task HandleAsync(SubmitGuideRequest req, CancellationToken } await guideService.SubmitForApprovalAsync(req.GuideId); + + var guideSlug = guideService.GetSlug(guide.Id); + + await auditLogService.LogAsync( + user.AccountId!.Value, + "guide_submitted", + "Guide", + guideSlug, + "Submitted guide for approval"); + + await notificationService.CreateAsync( + user.AccountId!.Value, + NotificationType.GuideSubmitted, + "Guide Submitted", + $"**{guide.DraftVersion?.Title}** has been submitted for approval!", + $"/guides/{guideSlug}?draft=true"); + await Send.NoContentAsync(ct); } } diff --git a/EliteAPI/Features/Guides/Services/GuideService.cs b/EliteAPI/Features/Guides/Services/GuideService.cs index a0312672..58b0fe4c 100644 --- a/EliteAPI/Features/Guides/Services/GuideService.cs +++ b/EliteAPI/Features/Guides/Services/GuideService.cs @@ -68,12 +68,12 @@ public async Task CreateDraftAsync(ulong authorId, GuideType type) public string GetSlug(int id) { - return EliteAPI.Features.Common.Services.SqidService.Encode(id); + return Common.Services.SqidService.Encode(id); } public int? GetIdFromSlug(string slug) { - return EliteAPI.Features.Common.Services.SqidService.Decode(slug); + return Common.Services.SqidService.Decode(slug); } public async Task UpdateDraftAsync(int guideId, string title, string description, string markdown, string? iconSkyblockId, List? tags, diff --git a/EliteAPI/Features/Monetization/Services/MonetizationService.cs b/EliteAPI/Features/Monetization/Services/MonetizationService.cs index da1c240e..7cb07201 100644 --- a/EliteAPI/Features/Monetization/Services/MonetizationService.cs +++ b/EliteAPI/Features/Monetization/Services/MonetizationService.cs @@ -14,6 +14,9 @@ using FastEndpoints; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; +using EliteAPI.Features.AuditLogs.Services; +using EliteAPI.Features.Notifications.Models; +using EliteAPI.Features.Notifications.Services; using Microsoft.Extensions.Options; using StackExchange.Redis; @@ -27,6 +30,8 @@ public class MonetizationService( ILogger logger, IBadgeService badgeService, IMessageService messageService, + NotificationService notificationService, + AuditLogService auditLogService, IOptions coolDowns) : IMonetizationService { @@ -140,6 +145,20 @@ public async Task GrantProductAccessAsync(ulong userId, ulong productId) { ); await context.SaveChangesAsync(); + + await auditLogService.LogAsync( + userId, + "shop_claim", + "Product", + productId.ToString(), + $"Claimed product {productId} {product?.Name ?? "Item"}"); + + await notificationService.CreateAsync( + userId, + NotificationType.ShopPurchase, + "Claim Successful", + $"You have claimed **{product?.Name ?? "Item"}**! Use your perk(s) in your account settings!", + $"/profile/settings"); } public async Task GrantTestEntitlementAsync(ulong targetId, ulong productId, @@ -435,6 +454,22 @@ public async Task SyncDiscordEntitlementsAsync(ulong entityId, bool isGuild) { discordEntitlement.UserId.ToString() ?? string.Empty, discordEntitlement.ProductId.ToString() ); + + var product = await context.Products.FindAsync(discordEntitlement.ProductId); + + await auditLogService.LogAsync( + discordEntitlement.UserId!.Value, + "shop_purchase", + "Product", + discordEntitlement.ProductId.ToString(), + $"Purchased product {discordEntitlement.ProductId} {product?.Name ?? "Item"}"); + + await notificationService.CreateAsync( + discordEntitlement.UserId.Value, + NotificationType.ShopPurchase, + "Purchase Successful", + $"Thank you for purchasing **{product?.Name ?? "Item"}**! Use your perk(s) in your account settings!", + $"/profile/settings"); } context.ProductAccesses.Add(access); diff --git a/EliteAPI/Features/Notifications/Models/NotificationType.cs b/EliteAPI/Features/Notifications/Models/NotificationType.cs index b8395400..86ca7886 100644 --- a/EliteAPI/Features/Notifications/Models/NotificationType.cs +++ b/EliteAPI/Features/Notifications/Models/NotificationType.cs @@ -15,5 +15,6 @@ public enum NotificationType CommentRejected = 7, NewComment = 8, NewReply = 9, - ShopPurchase = 10 + ShopPurchase = 10, + GuideSubmitted = 11 }