Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions backend/src/CCE.Api.External/Endpoints/FaqPublicEndpoints.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using CCE.Api.Common.Extensions;
using CCE.Application.PlatformSettings.Public.Queries.GetPublicFaqs;
using MediatR;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;

namespace CCE.Api.External.Endpoints;

public static class FaqPublicEndpoints
{
public static IEndpointRouteBuilder MapFaqPublicEndpoints(this IEndpointRouteBuilder app)
{
var faqs = app.MapGroup("/api/faqs").WithTags("FAQ");

faqs.MapGet("", async (IMediator mediator, CancellationToken ct) =>
{
var result = await mediator.Send(new GetPublicFaqsQuery(), ct).ConfigureAwait(false);
return result.ToHttpResult();
})
.AllowAnonymous()
.WithName("GetPublicFaqs");

return app;
}
}
1 change: 1 addition & 0 deletions backend/src/CCE.Api.External/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@
app.MapHomepageSettingsPublicEndpoints();
app.MapAboutSettingsPublicEndpoints();
app.MapPoliciesSettingsPublicEndpoints();
app.MapFaqPublicEndpoints();
app.MapMediaPublicEndpoints();
app.MapVerificationEndpoints();

Expand Down
96 changes: 96 additions & 0 deletions backend/src/CCE.Api.Internal/Endpoints/FaqEndpoints.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using CCE.Api.Common.Extensions;
using CCE.Application.Common;
using CCE.Application.PlatformSettings.Commands.CreateFaq;
using CCE.Application.PlatformSettings.Commands.DeleteFaq;
using CCE.Application.PlatformSettings.Commands.UpdateFaq;
using CCE.Application.PlatformSettings.Queries.GetFaqById;
using CCE.Application.PlatformSettings.Queries.GetFaqs;
using CCE.Domain;
using MediatR;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;

namespace CCE.Api.Internal.Endpoints;

public static class FaqEndpoints
{
public static IEndpointRouteBuilder MapFaqEndpoints(this IEndpointRouteBuilder app)
{
var faqs = app.MapGroup("/api/admin/settings/faqs").WithTags("PlatformSettings");

faqs.MapGet("", async (IMediator mediator, CancellationToken ct) =>
{
var result = await mediator.Send(new GetFaqsQuery(), ct).ConfigureAwait(false);
return result.ToHttpResult();
})
.RequireAuthorization(Permissions.Page_PolicyEdit)
.WithName("GetFaqs");

faqs.MapGet("/{id:guid}", async (
System.Guid id,
IMediator mediator, CancellationToken ct) =>
{
var result = await mediator.Send(new GetFaqByIdQuery(id), ct).ConfigureAwait(false);
return result.ToHttpResult();
})
.RequireAuthorization(Permissions.Page_PolicyEdit)
.WithName("GetFaqById");

faqs.MapPost("", async (
CreateFaqRequest body,
IMediator mediator, CancellationToken ct) =>
{
var cmd = new CreateFaqCommand(
body.QuestionAr, body.QuestionEn,
body.AnswerAr, body.AnswerEn,
body.Order);
var result = await mediator.Send(cmd, ct).ConfigureAwait(false);
return result.ToCreatedHttpResult();
})
.RequireAuthorization(Permissions.Page_PolicyEdit)
.WithName("CreateFaq");

faqs.MapPut("/{id:guid}", async (
System.Guid id,
UpdateFaqRequest body,
IMediator mediator, CancellationToken ct) =>
{
var cmd = new UpdateFaqCommand(
id,
body.QuestionAr, body.QuestionEn,
body.AnswerAr, body.AnswerEn,
body.Order);
var result = await mediator.Send(cmd, ct).ConfigureAwait(false);
return result.ToHttpResult();
})
.RequireAuthorization(Permissions.Page_PolicyEdit)
.WithName("UpdateFaq");

faqs.MapDelete("/{id:guid}", async (
System.Guid id,
IMediator mediator, CancellationToken ct) =>
{
var result = await mediator.Send(new DeleteFaqCommand(id), ct).ConfigureAwait(false);
return result.ToNoContentHttpResult();
})
.RequireAuthorization(Permissions.Page_PolicyEdit)
.WithName("DeleteFaq");

return app;
}
}

public sealed record CreateFaqRequest(
string QuestionAr,
string QuestionEn,
string AnswerAr,
string AnswerEn,
int Order = 0);

public sealed record UpdateFaqRequest(
string QuestionAr,
string QuestionEn,
string AnswerAr,
string AnswerEn,
int Order);
1 change: 1 addition & 0 deletions backend/src/CCE.Api.Internal/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
app.MapHomepageSettingsEndpoints();
app.MapAboutSettingsEndpoints();
app.MapPoliciesSettingsEndpoints();
app.MapFaqEndpoints();
app.MapMediaEndpoints();

// Sub-11d follow-up — dev sign-in shim. Mounts /dev/sign-in,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ public interface ICceDbContext
IQueryable<PoliciesSettings> PoliciesSettings { get; }
IQueryable<KnowledgePartner> KnowledgePartners { get; }
IQueryable<PolicySection> PolicySections { get; }
IQueryable<Faq> Faqs { get; }

// ─── Verification ───
IQueryable<OtpVerification> OtpVerifications { get; }
Expand Down
1 change: 1 addition & 0 deletions backend/src/CCE.Application/Messages/MessageFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ public FieldError Field(string fieldName, string domainKey)
public Response<T> GlossaryEntryNotFound<T>() => NotFound<T>("GLOSSARY_ENTRY_NOT_FOUND");
public Response<T> KnowledgePartnerNotFound<T>() => NotFound<T>("KNOWLEDGE_PARTNER_NOT_FOUND");
public Response<T> PolicySectionNotFound<T>() => NotFound<T>("POLICY_SECTION_NOT_FOUND");
public Response<T> FaqNotFound<T>() => NotFound<T>("FAQ_NOT_FOUND");
public Response<T> ContentUpdateFailed<T>() => BusinessRule<T>("CONTENT_UPDATE_FAILED");

// ─── Convenience shortcuts (Media domain) ───
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using CCE.Application.Common;
using MediatR;

namespace CCE.Application.PlatformSettings.Commands.CreateFaq;

public sealed record CreateFaqCommand(
string QuestionAr,
string QuestionEn,
string AnswerAr,
string AnswerEn,
int Order = 0) : IRequest<Response<System.Guid>>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using CCE.Application.Common;
using CCE.Application.Common.Interfaces;
using CCE.Application.Messages;
using CCE.Domain.Common;
using CCE.Domain.PlatformSettings;
using CCE.Domain.PlatformSettings.ValueObjects;
using MediatR;

namespace CCE.Application.PlatformSettings.Commands.CreateFaq;

public sealed class CreateFaqCommandHandler
: IRequestHandler<CreateFaqCommand, Response<System.Guid>>
{
private readonly IFaqRepository _repo;
private readonly ICceDbContext _db;
private readonly MessageFactory _msg;
private readonly ICurrentUserAccessor _currentUser;
private readonly ISystemClock _clock;

public CreateFaqCommandHandler(
IFaqRepository repo,
ICceDbContext db,
MessageFactory msg,
ICurrentUserAccessor currentUser,
ISystemClock clock)
{
_repo = repo;
_db = db;
_msg = msg;
_currentUser = currentUser;
_clock = clock;
}

public async Task<Response<System.Guid>> Handle(
CreateFaqCommand request, CancellationToken cancellationToken)
{
var userId = _currentUser.GetUserId()
?? throw new DomainException("User identity required.");
var question = LocalizedText.Create(request.QuestionAr, request.QuestionEn);
var answer = LocalizedText.Create(request.AnswerAr, request.AnswerEn);

var faq = Faq.Create(question, answer, request.Order, userId, _clock);
_repo.Add(faq);
await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);

return _msg.Ok(faq.Id, "CONTENT_CREATED");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using FluentValidation;

namespace CCE.Application.PlatformSettings.Commands.CreateFaq;

public sealed class CreateFaqCommandValidator
: AbstractValidator<CreateFaqCommand>
{
public CreateFaqCommandValidator()
{
RuleFor(x => x.QuestionAr).NotEmpty().MaximumLength(500);
RuleFor(x => x.QuestionEn).NotEmpty().MaximumLength(500);
RuleFor(x => x.AnswerAr).NotEmpty();
RuleFor(x => x.AnswerEn).NotEmpty();
RuleFor(x => x.Order).GreaterThanOrEqualTo(0);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
using CCE.Application.Common;
using MediatR;

namespace CCE.Application.PlatformSettings.Commands.DeleteFaq;

public sealed record DeleteFaqCommand(System.Guid Id) : IRequest<Response<VoidData>>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using CCE.Application.Common;
using CCE.Application.Common.Interfaces;
using CCE.Application.Messages;
using MediatR;

namespace CCE.Application.PlatformSettings.Commands.DeleteFaq;

public sealed class DeleteFaqCommandHandler
: IRequestHandler<DeleteFaqCommand, Response<VoidData>>
{
private readonly IFaqRepository _repo;
private readonly ICceDbContext _db;
private readonly MessageFactory _msg;

public DeleteFaqCommandHandler(IFaqRepository repo, ICceDbContext db, MessageFactory msg)
{
_repo = repo;
_db = db;
_msg = msg;
}

public async Task<Response<VoidData>> Handle(
DeleteFaqCommand request, CancellationToken cancellationToken)
{
var faq = await _repo.GetByIdAsync(request.Id, cancellationToken)
.ConfigureAwait(false);

if (faq is null)
return _msg.FaqNotFound<VoidData>();

_repo.Delete(faq);
await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);

return _msg.Ok("CONTENT_DELETED");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using FluentValidation;

namespace CCE.Application.PlatformSettings.Commands.DeleteFaq;

public sealed class DeleteFaqCommandValidator
: AbstractValidator<DeleteFaqCommand>
{
public DeleteFaqCommandValidator()
{
RuleFor(x => x.Id).NotEmpty();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using CCE.Application.Common;
using MediatR;

namespace CCE.Application.PlatformSettings.Commands.UpdateFaq;

public sealed record UpdateFaqCommand(
System.Guid Id,
string QuestionAr,
string QuestionEn,
string AnswerAr,
string AnswerEn,
int Order) : IRequest<Response<System.Guid>>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using CCE.Application.Common;
using CCE.Application.Common.Interfaces;
using CCE.Application.Messages;
using CCE.Domain.Common;
using CCE.Domain.PlatformSettings;
using CCE.Domain.PlatformSettings.ValueObjects;
using MediatR;

namespace CCE.Application.PlatformSettings.Commands.UpdateFaq;

public sealed class UpdateFaqCommandHandler
: IRequestHandler<UpdateFaqCommand, Response<System.Guid>>
{
private readonly IFaqRepository _repo;
private readonly ICceDbContext _db;
private readonly MessageFactory _msg;
private readonly ICurrentUserAccessor _currentUser;
private readonly ISystemClock _clock;

public UpdateFaqCommandHandler(
IFaqRepository repo,
ICceDbContext db,
MessageFactory msg,
ICurrentUserAccessor currentUser,
ISystemClock clock)
{
_repo = repo;
_db = db;
_msg = msg;
_currentUser = currentUser;
_clock = clock;
}

public async Task<Response<System.Guid>> Handle(
UpdateFaqCommand request, CancellationToken cancellationToken)
{
var faq = await _repo.GetByIdAsync(request.Id, cancellationToken)
.ConfigureAwait(false);

if (faq is null)
return _msg.FaqNotFound<System.Guid>();

var userId = _currentUser.GetUserId()
?? throw new DomainException("User identity required.");
var question = LocalizedText.Create(request.QuestionAr, request.QuestionEn);
var answer = LocalizedText.Create(request.AnswerAr, request.AnswerEn);

faq.UpdateContent(question, answer, request.Order, userId, _clock);
await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);

return _msg.Ok(faq.Id, "CONTENT_UPDATED");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using FluentValidation;

namespace CCE.Application.PlatformSettings.Commands.UpdateFaq;

public sealed class UpdateFaqCommandValidator
: AbstractValidator<UpdateFaqCommand>
{
public UpdateFaqCommandValidator()
{
RuleFor(x => x.Id).NotEmpty();
RuleFor(x => x.QuestionAr).NotEmpty().MaximumLength(500);
RuleFor(x => x.QuestionEn).NotEmpty().MaximumLength(500);
RuleFor(x => x.AnswerAr).NotEmpty();
RuleFor(x => x.AnswerEn).NotEmpty();
RuleFor(x => x.Order).GreaterThanOrEqualTo(0);
}
}
7 changes: 7 additions & 0 deletions backend/src/CCE.Application/PlatformSettings/Dtos/FaqDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace CCE.Application.PlatformSettings.Dtos;

public sealed record FaqDto(
System.Guid Id,
LocalizedTextDto Question,
LocalizedTextDto Answer,
int Order);
11 changes: 11 additions & 0 deletions backend/src/CCE.Application/PlatformSettings/IFaqRepository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using CCE.Domain.PlatformSettings;

namespace CCE.Application.PlatformSettings;

/// <summary>Repository for the standalone FAQ entity.</summary>
public interface IFaqRepository
{
Task<Faq?> GetByIdAsync(System.Guid id, CancellationToken ct);
void Add(Faq faq);
void Delete(Faq faq);
}
Loading
Loading