Skip to content

Commit 38ab97f

Browse files
committed
feat: add FAQ CRUD administration endpoints and application logic
1 parent d65c5c5 commit 38ab97f

24 files changed

Lines changed: 4413 additions & 0 deletions
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
using CCE.Api.Common.Extensions;
2+
using CCE.Application.Common;
3+
using CCE.Application.PlatformSettings.Commands.CreateFaq;
4+
using CCE.Application.PlatformSettings.Commands.DeleteFaq;
5+
using CCE.Application.PlatformSettings.Commands.UpdateFaq;
6+
using CCE.Application.PlatformSettings.Queries.GetFaqById;
7+
using CCE.Application.PlatformSettings.Queries.GetFaqs;
8+
using CCE.Domain;
9+
using MediatR;
10+
using Microsoft.AspNetCore.Builder;
11+
using Microsoft.AspNetCore.Http;
12+
using Microsoft.AspNetCore.Routing;
13+
14+
namespace CCE.Api.Internal.Endpoints;
15+
16+
public static class FaqEndpoints
17+
{
18+
public static IEndpointRouteBuilder MapFaqEndpoints(this IEndpointRouteBuilder app)
19+
{
20+
var faqs = app.MapGroup("/api/admin/settings/faqs").WithTags("PlatformSettings");
21+
22+
faqs.MapGet("", async (IMediator mediator, CancellationToken ct) =>
23+
{
24+
var result = await mediator.Send(new GetFaqsQuery(), ct).ConfigureAwait(false);
25+
return result.ToHttpResult();
26+
})
27+
.RequireAuthorization(Permissions.Page_PolicyEdit)
28+
.WithName("GetFaqs");
29+
30+
faqs.MapGet("/{id:guid}", async (
31+
System.Guid id,
32+
IMediator mediator, CancellationToken ct) =>
33+
{
34+
var result = await mediator.Send(new GetFaqByIdQuery(id), ct).ConfigureAwait(false);
35+
return result.ToHttpResult();
36+
})
37+
.RequireAuthorization(Permissions.Page_PolicyEdit)
38+
.WithName("GetFaqById");
39+
40+
faqs.MapPost("", async (
41+
CreateFaqRequest body,
42+
IMediator mediator, CancellationToken ct) =>
43+
{
44+
var cmd = new CreateFaqCommand(
45+
body.QuestionAr, body.QuestionEn,
46+
body.AnswerAr, body.AnswerEn,
47+
body.Order);
48+
var result = await mediator.Send(cmd, ct).ConfigureAwait(false);
49+
return result.ToCreatedHttpResult();
50+
})
51+
.RequireAuthorization(Permissions.Page_PolicyEdit)
52+
.WithName("CreateFaq");
53+
54+
faqs.MapPut("/{id:guid}", async (
55+
System.Guid id,
56+
UpdateFaqRequest body,
57+
IMediator mediator, CancellationToken ct) =>
58+
{
59+
var cmd = new UpdateFaqCommand(
60+
id,
61+
body.QuestionAr, body.QuestionEn,
62+
body.AnswerAr, body.AnswerEn,
63+
body.Order);
64+
var result = await mediator.Send(cmd, ct).ConfigureAwait(false);
65+
return result.ToHttpResult();
66+
})
67+
.RequireAuthorization(Permissions.Page_PolicyEdit)
68+
.WithName("UpdateFaq");
69+
70+
faqs.MapDelete("/{id:guid}", async (
71+
System.Guid id,
72+
IMediator mediator, CancellationToken ct) =>
73+
{
74+
var result = await mediator.Send(new DeleteFaqCommand(id), ct).ConfigureAwait(false);
75+
return result.ToNoContentHttpResult();
76+
})
77+
.RequireAuthorization(Permissions.Page_PolicyEdit)
78+
.WithName("DeleteFaq");
79+
80+
return app;
81+
}
82+
}
83+
84+
public sealed record CreateFaqRequest(
85+
string QuestionAr,
86+
string QuestionEn,
87+
string AnswerAr,
88+
string AnswerEn,
89+
int Order = 0);
90+
91+
public sealed record UpdateFaqRequest(
92+
string QuestionAr,
93+
string QuestionEn,
94+
string AnswerAr,
95+
string AnswerEn,
96+
int Order);

backend/src/CCE.Api.Internal/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
app.MapHomepageSettingsEndpoints();
8686
app.MapAboutSettingsEndpoints();
8787
app.MapPoliciesSettingsEndpoints();
88+
app.MapFaqEndpoints();
8889
app.MapMediaEndpoints();
8990

9091
// Sub-11d follow-up — dev sign-in shim. Mounts /dev/sign-in,

backend/src/CCE.Application/Common/Interfaces/ICceDbContext.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ public interface ICceDbContext
7070
IQueryable<PoliciesSettings> PoliciesSettings { get; }
7171
IQueryable<KnowledgePartner> KnowledgePartners { get; }
7272
IQueryable<PolicySection> PolicySections { get; }
73+
IQueryable<Faq> Faqs { get; }
7374

7475
// ─── Verification ───
7576
IQueryable<OtpVerification> OtpVerifications { get; }

backend/src/CCE.Application/Messages/MessageFactory.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ public FieldError Field(string fieldName, string domainKey)
8787
public Response<T> GlossaryEntryNotFound<T>() => NotFound<T>("GLOSSARY_ENTRY_NOT_FOUND");
8888
public Response<T> KnowledgePartnerNotFound<T>() => NotFound<T>("KNOWLEDGE_PARTNER_NOT_FOUND");
8989
public Response<T> PolicySectionNotFound<T>() => NotFound<T>("POLICY_SECTION_NOT_FOUND");
90+
public Response<T> FaqNotFound<T>() => NotFound<T>("FAQ_NOT_FOUND");
9091
public Response<T> ContentUpdateFailed<T>() => BusinessRule<T>("CONTENT_UPDATE_FAILED");
9192

9293
// ─── Convenience shortcuts (Media domain) ───
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using CCE.Application.Common;
2+
using MediatR;
3+
4+
namespace CCE.Application.PlatformSettings.Commands.CreateFaq;
5+
6+
public sealed record CreateFaqCommand(
7+
string QuestionAr,
8+
string QuestionEn,
9+
string AnswerAr,
10+
string AnswerEn,
11+
int Order = 0) : IRequest<Response<System.Guid>>;
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
using CCE.Application.Common;
2+
using CCE.Application.Common.Interfaces;
3+
using CCE.Application.Messages;
4+
using CCE.Domain.Common;
5+
using CCE.Domain.PlatformSettings;
6+
using CCE.Domain.PlatformSettings.ValueObjects;
7+
using MediatR;
8+
9+
namespace CCE.Application.PlatformSettings.Commands.CreateFaq;
10+
11+
public sealed class CreateFaqCommandHandler
12+
: IRequestHandler<CreateFaqCommand, Response<System.Guid>>
13+
{
14+
private readonly ICceDbContext _db;
15+
private readonly MessageFactory _msg;
16+
private readonly ICurrentUserAccessor _currentUser;
17+
private readonly ISystemClock _clock;
18+
19+
public CreateFaqCommandHandler(
20+
ICceDbContext db,
21+
MessageFactory msg,
22+
ICurrentUserAccessor currentUser,
23+
ISystemClock clock)
24+
{
25+
_db = db;
26+
_msg = msg;
27+
_currentUser = currentUser;
28+
_clock = clock;
29+
}
30+
31+
public async Task<Response<System.Guid>> Handle(
32+
CreateFaqCommand request, CancellationToken cancellationToken)
33+
{
34+
var userId = _currentUser.GetUserId()
35+
?? throw new DomainException("User identity required.");
36+
var question = LocalizedText.Create(request.QuestionAr, request.QuestionEn);
37+
var answer = LocalizedText.Create(request.AnswerAr, request.AnswerEn);
38+
39+
var faq = Faq.Create(question, answer, request.Order, userId, _clock);
40+
_db.Add(faq);
41+
await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
42+
43+
return _msg.Ok(faq.Id, "CONTENT_CREATED");
44+
}
45+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using FluentValidation;
2+
3+
namespace CCE.Application.PlatformSettings.Commands.CreateFaq;
4+
5+
public sealed class CreateFaqCommandValidator
6+
: AbstractValidator<CreateFaqCommand>
7+
{
8+
public CreateFaqCommandValidator()
9+
{
10+
RuleFor(x => x.QuestionAr).NotEmpty().MaximumLength(500);
11+
RuleFor(x => x.QuestionEn).NotEmpty().MaximumLength(500);
12+
RuleFor(x => x.AnswerAr).NotEmpty();
13+
RuleFor(x => x.AnswerEn).NotEmpty();
14+
RuleFor(x => x.Order).GreaterThanOrEqualTo(0);
15+
}
16+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
using CCE.Application.Common;
2+
using MediatR;
3+
4+
namespace CCE.Application.PlatformSettings.Commands.DeleteFaq;
5+
6+
public sealed record DeleteFaqCommand(System.Guid Id) : IRequest<Response<VoidData>>;
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using CCE.Application.Common;
2+
using CCE.Application.Common.Interfaces;
3+
using CCE.Application.Messages;
4+
using MediatR;
5+
using Microsoft.EntityFrameworkCore;
6+
7+
namespace CCE.Application.PlatformSettings.Commands.DeleteFaq;
8+
9+
public sealed class DeleteFaqCommandHandler
10+
: IRequestHandler<DeleteFaqCommand, Response<VoidData>>
11+
{
12+
private readonly ICceDbContext _db;
13+
private readonly MessageFactory _msg;
14+
15+
public DeleteFaqCommandHandler(ICceDbContext db, MessageFactory msg)
16+
{
17+
_db = db;
18+
_msg = msg;
19+
}
20+
21+
public async Task<Response<VoidData>> Handle(
22+
DeleteFaqCommand request, CancellationToken cancellationToken)
23+
{
24+
var faq = await _db.Faqs
25+
.FirstOrDefaultAsync(f => f.Id == request.Id, cancellationToken)
26+
.ConfigureAwait(false);
27+
28+
if (faq is null)
29+
return _msg.FaqNotFound<VoidData>();
30+
31+
_db.Delete(faq);
32+
await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
33+
34+
return _msg.Ok("CONTENT_DELETED");
35+
}
36+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using FluentValidation;
2+
3+
namespace CCE.Application.PlatformSettings.Commands.DeleteFaq;
4+
5+
public sealed class DeleteFaqCommandValidator
6+
: AbstractValidator<DeleteFaqCommand>
7+
{
8+
public DeleteFaqCommandValidator()
9+
{
10+
RuleFor(x => x.Id).NotEmpty();
11+
}
12+
}

0 commit comments

Comments
 (0)