Skip to content
Closed
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
1,428 changes: 1,428 additions & 0 deletions backend/AddServiceEvaluation.sql

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions backend/src/CCE.Api.Common/Localization/Resources.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -456,3 +456,11 @@ NOTIFICATION_TEMPLATE_CREATED:
NOTIFICATION_TEMPLATE_UPDATED:
ar: "تم تحديث قالب الإشعار بنجاح"
en: "Notification template updated successfully"

EVALUATION_NOT_FOUND:
ar: "التقييم غير موجود"
en: "Evaluation not found"

EVALUATION_SUBMITTED:
ar: "تم تقديم التقييم بنجاح"
en: "Evaluation submitted successfully"
42 changes: 42 additions & 0 deletions backend/src/CCE.Api.External/Endpoints/EvaluationEndpoints.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using CCE.Api.Common.Extensions;
using CCE.Application.Evaluation.Commands.SubmitEvaluation;
using CCE.Domain.Evaluation;
using MediatR;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;

namespace CCE.Api.External.Endpoints;

public static class EvaluationEndpoints
{
public static IEndpointRouteBuilder MapEvaluationEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/evaluations").WithTags("Evaluations");

// POST /api/evaluations — public submit (visitors & authenticated users)
group.MapPost("", async (
SubmitEvaluationRequest body,
IMediator mediator,
CancellationToken ct) =>
{
var cmd = new SubmitEvaluationCommand(
body.OverallSatisfaction,
body.EaseOfUse,
body.ContentSuitability,
body.Feedback);
var result = await mediator.Send(cmd, ct).ConfigureAwait(false);
return result.ToHttpResult(StatusCodes.Status201Created);
})
.AllowAnonymous()
.WithName("SubmitEvaluation");

return app;
}
}

public sealed record SubmitEvaluationRequest(
EvaluationRating OverallSatisfaction,
EvaluationRating EaseOfUse,
EvaluationRating ContentSuitability,
string Feedback);
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 @@ -113,6 +113,7 @@
app.MapAssistantEndpoints();
app.MapKapsarcEndpoints();
app.MapSurveysEndpoints();
app.MapEvaluationEndpoints();
app.MapHomepageSettingsPublicEndpoints();
app.MapAboutSettingsPublicEndpoints();
app.MapPoliciesSettingsPublicEndpoints();
Expand Down
42 changes: 42 additions & 0 deletions backend/src/CCE.Api.Internal/Endpoints/EvaluationEndpoints.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using CCE.Application.Evaluation.Queries.GetAllEvaluations;
using CCE.Application.Evaluation.Queries.GetEvaluationById;
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 EvaluationEndpoints
{
public static IEndpointRouteBuilder MapEvaluationEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/admin/evaluations").WithTags("Evaluations");

// GET /api/admin/evaluations — list all (admin only)
group.MapGet("", async (
IMediator mediator,
CancellationToken ct) =>
{
var result = await mediator.Send(new GetAllEvaluationsQuery(), ct).ConfigureAwait(false);
return Results.Ok(result);
})
.RequireAuthorization(Permissions.Survey_ReadAll)
.WithName("GetAllEvaluations");

// GET /api/admin/evaluations/{id} — get by id (admin only)
group.MapGet("{id:guid}", async (
System.Guid id,
IMediator mediator,
CancellationToken ct) =>
{
var result = await mediator.Send(new GetEvaluationByIdQuery(id), ct).ConfigureAwait(false);
return result is null ? Results.NotFound() : Results.Ok(result);
})
.RequireAuthorization(Permissions.Survey_ReadAll)
.WithName("GetEvaluationById");

return app;
}
}
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 @@ -86,6 +86,7 @@
app.MapAboutSettingsEndpoints();
app.MapPoliciesSettingsEndpoints();
app.MapMediaEndpoints();
app.MapEvaluationEndpoints();

// Sub-11d follow-up — dev sign-in shim. Mounts /dev/sign-in,
// /dev/sign-out, /dev/whoami when Auth:DevMode=true. Production
Expand Down
3 changes: 3 additions & 0 deletions backend/src/CCE.Application/Common/Errors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ public Error RegistrationFailed(IDictionary<string, string[]>? details = null)
public Error PostNotFound() => NotFound($"COMMUNITY_{ApplicationErrors.Community.POST_NOT_FOUND}");
public Error ReplyNotFound() => NotFound($"COMMUNITY_{ApplicationErrors.Community.REPLY_NOT_FOUND}");

// ─── Convenience: Evaluation domain ───
public Error EvaluationNotFound() => NotFound($"EVALUATION_{ApplicationErrors.Evaluation.EVALUATION_NOT_FOUND}");

// ─── Convenience: Country domain ───
public Error CountryNotFound() => NotFound($"COUNTRY_{ApplicationErrors.Country.COUNTRY_NOT_FOUND}");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using CCE.Domain.Audit;
using CCE.Domain.Community;
using CCE.Domain.Content;
using CCE.Domain.Evaluation;
using CCE.Domain.Identity;
using CCE.Domain.InteractiveCity;
using CCE.Domain.KnowledgeMaps;
Expand Down Expand Up @@ -75,6 +76,9 @@ public interface ICceDbContext
IQueryable<OtpVerification> OtpVerifications { get; }
IQueryable<UserVerification> UserVerifications { get; }

// ─── Evaluation ───
IQueryable<ServiceEvaluation> ServiceEvaluations { get; }

// ─── Media ───
IQueryable<MediaFile> MediaFiles { get; }

Expand Down
6 changes: 6 additions & 0 deletions backend/src/CCE.Application/Errors/ApplicationErrors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@ public static class InteractiveCity
public const string TECHNOLOGY_NOT_FOUND = "TECHNOLOGY_NOT_FOUND";
}

public static class Evaluation
{
public const string EVALUATION_NOT_FOUND = "EVALUATION_NOT_FOUND";
public const string EVALUATION_SUBMITTED = "EVALUATION_SUBMITTED";
}

public static class Validation
{
public const string REQUIRED_FIELD = "REQUIRED_FIELD";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using CCE.Application.Common;
using CCE.Domain.Evaluation;
using MediatR;

namespace CCE.Application.Evaluation.Commands.SubmitEvaluation;

public sealed record SubmitEvaluationCommand(
EvaluationRating OverallSatisfaction,
EvaluationRating EaseOfUse,
EvaluationRating ContentSuitability,
string Feedback) : IRequest<Response<VoidData>>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using CCE.Application.Common;
using CCE.Application.Common.Interfaces;
using CCE.Application.Errors;
using CCE.Application.Messages;
using CCE.Domain.Common;
using DomainEvaluation = CCE.Domain.Evaluation.ServiceEvaluation;
using MediatR;

namespace CCE.Application.Evaluation.Commands.SubmitEvaluation;

public sealed class SubmitEvaluationCommandHandler
: IRequestHandler<SubmitEvaluationCommand, Response<VoidData>>
{
private readonly IEvaluationRepository _repository;
private readonly ICurrentUserAccessor _currentUser;
private readonly ISystemClock _clock;
private readonly MessageFactory _messageFactory;

public SubmitEvaluationCommandHandler(
IEvaluationRepository repository,
ICurrentUserAccessor currentUser,
ISystemClock clock,
MessageFactory messageFactory)
{
_repository = repository;
_currentUser = currentUser;
_clock = clock;
_messageFactory = messageFactory;
}

public async Task<Response<VoidData>> Handle(
SubmitEvaluationCommand request,
CancellationToken cancellationToken)
{
var userId = _currentUser.GetUserId();

var evaluation = DomainEvaluation.Submit(
request.OverallSatisfaction,
request.EaseOfUse,
request.ContentSuitability,
request.Feedback,
userId,
_clock);

await _repository.AddAsync(evaluation, cancellationToken).ConfigureAwait(false);

return _messageFactory.Ok(ApplicationErrors.Evaluation.EVALUATION_SUBMITTED);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using CCE.Domain.Evaluation;
using FluentValidation;

namespace CCE.Application.Evaluation.Commands.SubmitEvaluation;

public sealed class SubmitEvaluationCommandValidator : AbstractValidator<SubmitEvaluationCommand>
{
public SubmitEvaluationCommandValidator()
{
RuleFor(x => x.OverallSatisfaction).IsInEnum().NotEqual(EvaluationRating.None);
RuleFor(x => x.EaseOfUse).IsInEnum().NotEqual(EvaluationRating.None);
RuleFor(x => x.ContentSuitability).IsInEnum().NotEqual(EvaluationRating.None);
RuleFor(x => x.Feedback).NotEmpty().MaximumLength(500);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using CCE.Domain.Evaluation;

namespace CCE.Application.Evaluation.DTOs;

public sealed record ServiceEvaluationDto(
System.Guid Id,
EvaluationRating OverallSatisfaction,
EvaluationRating EaseOfUse,
EvaluationRating ContentSuitability,
string Feedback,
System.Guid? UserId,
System.DateTimeOffset CreatedOn,
System.Guid CreatedById);
10 changes: 10 additions & 0 deletions backend/src/CCE.Application/Evaluation/IEvaluationRepository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using CCE.Domain.Evaluation;

namespace CCE.Application.Evaluation;

public interface IEvaluationRepository
{
Task AddAsync(ServiceEvaluation evaluation, CancellationToken ct);
Task<List<ServiceEvaluation>> GetAllAsync(CancellationToken ct);
Task<ServiceEvaluation?> GetByIdAsync(System.Guid id, CancellationToken ct);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
using CCE.Application.Evaluation.DTOs;
using MediatR;

namespace CCE.Application.Evaluation.Queries.GetAllEvaluations;

public sealed record GetAllEvaluationsQuery : IRequest<List<ServiceEvaluationDto>>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using CCE.Application.Evaluation.DTOs;
using MediatR;

namespace CCE.Application.Evaluation.Queries.GetAllEvaluations;

public sealed class GetAllEvaluationsQueryHandler
: IRequestHandler<GetAllEvaluationsQuery, List<ServiceEvaluationDto>>
{
private readonly IEvaluationRepository _repository;

public GetAllEvaluationsQueryHandler(IEvaluationRepository repository)
{
_repository = repository;
}

public async Task<List<ServiceEvaluationDto>> Handle(
GetAllEvaluationsQuery request,
CancellationToken cancellationToken)
{
var evaluations = await _repository.GetAllAsync(cancellationToken).ConfigureAwait(false);

return evaluations.Select(e => new ServiceEvaluationDto(
e.Id,
e.OverallSatisfaction,
e.EaseOfUse,
e.ContentSuitability,
e.Feedback,
e.UserId,
e.CreatedOn,
e.CreatedById)).ToList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
using CCE.Application.Evaluation.DTOs;
using MediatR;

namespace CCE.Application.Evaluation.Queries.GetEvaluationById;

public sealed record GetEvaluationByIdQuery(System.Guid Id) : IRequest<ServiceEvaluationDto?>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using CCE.Application.Evaluation.DTOs;
using MediatR;

namespace CCE.Application.Evaluation.Queries.GetEvaluationById;

public sealed class GetEvaluationByIdQueryHandler
: IRequestHandler<GetEvaluationByIdQuery, ServiceEvaluationDto?>
{
private readonly IEvaluationRepository _repository;

public GetEvaluationByIdQueryHandler(IEvaluationRepository repository)
{
_repository = repository;
}

public async Task<ServiceEvaluationDto?> Handle(
GetEvaluationByIdQuery request,
CancellationToken cancellationToken)
{
var evaluation = await _repository.GetByIdAsync(request.Id, cancellationToken).ConfigureAwait(false);

if (evaluation is null) return null;

return new ServiceEvaluationDto(
evaluation.Id,
evaluation.OverallSatisfaction,
evaluation.EaseOfUse,
evaluation.ContentSuitability,
evaluation.Feedback,
evaluation.UserId,
evaluation.CreatedOn,
evaluation.CreatedById);
}
}
5 changes: 5 additions & 0 deletions backend/src/CCE.Application/Messages/MessageFactory.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using CCE.Application.Common;
using CCE.Application.Errors;
using CCE.Application.Localization;
using CCE.Domain.Common;

Expand Down Expand Up @@ -105,6 +106,10 @@ public FieldError Field(string fieldName, string domainKey)
public Response<T> OtpCooldownActive<T>() => BusinessRule<T>("OTP_COOLDOWN_ACTIVE");
public Response<T> OtpInvalidated<T>() => BusinessRule<T>("OTP_INVALIDATED");

// ─── Convenience shortcuts (Evaluation domain) ───
public Response<VoidData> EvaluationSubmitted() => Ok(ApplicationErrors.Evaluation.EVALUATION_SUBMITTED);
public Response<T> EvaluationNotFound<T>() => NotFound<T>(ApplicationErrors.Evaluation.EVALUATION_NOT_FOUND);

// ─── Convenience shortcuts (Notification domain) ───

public Response<T> NotificationTemplateNotFound<T>() => NotFound<T>("TEMPLATE_NOT_FOUND");
Expand Down
3 changes: 3 additions & 0 deletions backend/src/CCE.Application/Messages/SystemCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ public static class SystemCode
public const string ERR124 = "ERR124"; // OTP cooldown active
public const string ERR125 = "ERR125"; // OTP invalidated

// ─── Evaluation Errors ───
public const string ERR009 = "ERR009"; // Evaluation not found

// ─── General Errors ───
public const string ERR900 = "ERR900"; // Internal server error
public const string ERR901 = "ERR901"; // Unauthorized access
Expand Down
6 changes: 6 additions & 0 deletions backend/src/CCE.Application/Messages/SystemCodeMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ public static class SystemCodeMap
["OTP_COOLDOWN_ACTIVE"] = SystemCode.ERR124,
["OTP_INVALIDATED"] = SystemCode.ERR125,

// ─── Evaluation Errors ───
["EVALUATION_NOT_FOUND"] = SystemCode.ERR009,

// ─── General Errors ───
["INTERNAL_ERROR"] = SystemCode.ERR900,
["UNAUTHORIZED_ACCESS"] = SystemCode.ERR901,
Expand Down Expand Up @@ -156,6 +159,9 @@ public static class SystemCodeMap
["OTP_SENT"] = SystemCode.CON060,
["OTP_VERIFIED"] = SystemCode.CON061,

// ─── Evaluation Success ───
["EVALUATION_SUBMITTED"] = SystemCode.CON008,

// ─── General Success ───
["ITEMS_LISTED"] = SystemCode.CON100,
["SUCCESS_OPERATION"] = SystemCode.CON900,
Expand Down
11 changes: 11 additions & 0 deletions backend/src/CCE.Domain/Evaluation/EvaluationRating.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace CCE.Domain.Evaluation;

public enum EvaluationRating
{
None = 0,
Excellent = 1,
Satisfied = 2,
Neutral = 3,
Dissatisfied = 4,
Poor = 5
}
Loading