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
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 @@ -335,3 +335,11 @@ SCENARIO_NOT_FOUND:
TECHNOLOGY_NOT_FOUND:
ar: "التقنية غير موجودة"
en: "Technology not found"

INTEREST_NOT_FOUND:
ar: "الاهتمام غير موجود"
en: "Interest not found"

INTEREST_UPSERTED:
ar: "تم تحديث الاهتمامات بنجاح"
en: "Interests updated successfully"
36 changes: 36 additions & 0 deletions backend/src/CCE.Api.External/Endpoints/UserInterestEndpoints.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using CCE.Api.Common.Extensions;
using CCE.Application.Common.Interfaces;
using CCE.Application.Identity.Public.Commands.UserInterest;
using MediatR;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;

namespace CCE.Api.External.Endpoints;

public static class UserInterestEndpoints
{
public static IEndpointRouteBuilder MapUserInterestEndpoints(this IEndpointRouteBuilder app)
{
var me = app.MapGroup("/api/me").WithTags("User Interests").RequireAuthorization();

me.MapPatch("/interests", async (
UpsertUserInterestRequest body,
ICurrentUserAccessor currentUser,
IMediator mediator,
CancellationToken ct) =>
{
var userId = currentUser.GetUserId() ?? System.Guid.Empty;
if (userId == System.Guid.Empty) return Results.Unauthorized();

var result = await mediator.Send(
new UpsertUserInterestCommand(userId, body.Interests), ct).ConfigureAwait(false);
return result.ToHttpResult();
})
.WithName("UpsertUserInterest");

return app;
}
}

public sealed record UpsertUserInterestRequest(IReadOnlyList<string> Interests);
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 @@ -103,6 +103,7 @@
app.MapAssistantEndpoints();
app.MapKapsarcEndpoints();
app.MapSurveysEndpoints();
app.MapUserInterestEndpoints();

app.MapGet("/health", async (IMediator mediator) =>
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using CCE.Application.Common;
using MediatR;

namespace CCE.Application.Identity.Public.Commands.UserInterest;

public sealed record UpsertUserInterestCommand(
System.Guid UserId,
IReadOnlyList<string> Interests) : IRequest<Response<UpsertUserInterestResult>>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using System.Linq;
using CCE.Application.Common;
using CCE.Application.Common.Interfaces;
using CCE.Application.Messages;
using MediatR;

namespace CCE.Application.Identity.Public.Commands.UserInterest;

public sealed class UpsertUserInterestCommandHandler
: IRequestHandler<UpsertUserInterestCommand, Response<UpsertUserInterestResult>>
{
private readonly IUserProfileRepository _service;
private readonly ICceDbContext _db;
private readonly MessageFactory _msg;

public UpsertUserInterestCommandHandler(
IUserProfileRepository service,
ICceDbContext db,
MessageFactory msg)
{
_service = service;
_db = db;
_msg = msg;
}

public async Task<Response<UpsertUserInterestResult>> Handle(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use StringComparer.OrdinalIgnoreCase in Except() to avoid false add/remove detection caused by casing differences.
skipping _service.Update(user) and SaveChangesAsync() when nothing changed can avoid unnecessary db writes.

UpsertUserInterestCommand request,
CancellationToken cancellationToken)
{
var user = await _service.FindAsync(request.UserId, cancellationToken).ConfigureAwait(false);
if (user is null)
return _msg.UserNotFound<UpsertUserInterestResult>();

var oldInterests = user.Interests.ToList();
var rawList = request.Interests ?? System.Array.Empty<string>();

var normalizedNew = rawList
.Select(static s => s?.Trim() ?? string.Empty)
.Where(static s => s.Length > 0)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();

var oldSet = new HashSet<string>(oldInterests, StringComparer.OrdinalIgnoreCase);
var newSet = new HashSet<string>(normalizedNew, StringComparer.OrdinalIgnoreCase);

if (oldSet.SetEquals(newSet))
{
return _msg.InterestUpserted(new UpsertUserInterestResult(
user.Interests,
System.Array.Empty<string>(),
System.Array.Empty<string>()));
}

user.UpdateInterests(normalizedNew);

var added = normalizedNew.Except(oldInterests, StringComparer.OrdinalIgnoreCase).ToList();
var removed = oldInterests.Except(normalizedNew, StringComparer.OrdinalIgnoreCase).ToList();

_service.Update(user);
await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);

return _msg.InterestUpserted(new UpsertUserInterestResult(
user.Interests,
added,
removed));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace CCE.Application.Identity.Public.Commands.UserInterest;

public sealed record UpsertUserInterestResult(
IReadOnlyList<string> Interests,
IReadOnlyList<string> Added,
IReadOnlyList<string> Removed);
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 @@ -68,6 +68,7 @@ public FieldError Field(string fieldName, string domainKey)
// ─── Convenience shortcuts (Identity domain) ───

public Response<T> UserNotFound<T>() => NotFound<T>("USER_NOT_FOUND");
public Response<T> InterestUpserted<T>(T data) => Ok(data, "INTEREST_UPSERTED");
public Response<T> EmailExists<T>() => Conflict<T>("EMAIL_EXISTS");
public Response<T> InvalidCredentials<T>() => Unauthorized<T>("INVALID_CREDENTIALS");
public Response<T> NotAuthenticated<T>() => Unauthorized<T>("NOT_AUTHENTICATED");
Expand Down
2 changes: 2 additions & 0 deletions backend/src/CCE.Application/Messages/SystemCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public static class SystemCode
public const string ERR002 = "ERR002"; // Resource download failure (appendix)
public const string ERR003 = "ERR003"; // Resource share failure (appendix)

public const string ERR018 = "ERR018"; // Interest not found
public const string ERR019 = "ERR019"; // Email already exists / Account creation failure (appendix)
public const string ERR020 = "ERR020"; // Invalid credentials (appendix)
public const string ERR021 = "ERR021"; // Login system error (appendix)
Expand Down Expand Up @@ -115,6 +116,7 @@ public static class SystemCode
public const string CON007 = "CON007"; // Admin notified of expert request (appendix)
public const string CON008 = "CON008"; // Service evaluation submitted (appendix)
public const string CON009 = "CON009"; // Personalized suggestions submitted (appendix)
public const string CON018 = "CON018"; // Interest upserted
public const string CON010 = "CON010"; // Topic follow success (appendix)
public const string CON011 = "CON011"; // Post created (appendix)
public const string CON012 = "CON012"; // Post follow success (appendix)
Expand Down
2 changes: 2 additions & 0 deletions backend/src/CCE.Application/Messages/SystemCodeMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public static class SystemCodeMap
["INVALID_CREDENTIALS"] = SystemCode.ERR020,
["PASSWORD_RECOVERY_FAILED"] = SystemCode.ERR023,
["LOGOUT_FAILED"] = SystemCode.ERR024,
["INTEREST_NOT_FOUND"] = SystemCode.ERR018,

// ─── Backend-only Identity Errors (moved to free appendix numbers) ───
["EXPERT_REQUEST_NOT_FOUND"] = SystemCode.ERR400,
Expand Down Expand Up @@ -90,6 +91,7 @@ public static class SystemCodeMap
["PASSWORD_RESET"] = SystemCode.CON014,
["LOGOUT_SUCCESS"] = SystemCode.CON015,
["REGISTER_SUCCESS"] = SystemCode.CON017,
["INTEREST_UPSERTED"] = SystemCode.CON018,

// ─── Backend-only Identity Success (appendix numbers already taken) ───
["EXPERT_REQUEST_APPROVED"] = SystemCode.CON050,
Expand Down