diff --git a/.github/workflows/build_microservices.yml b/.github/workflows/build_microservices.yml index 2d60a935..d53f07a0 100644 --- a/.github/workflows/build_microservices.yml +++ b/.github/workflows/build_microservices.yml @@ -1,6 +1,10 @@ name: .NET MiniSpace Microservice build & integration -on: [push, pull_request] +on: + push: + branches: + - '*' + pull_request: jobs: build-and-test: @@ -12,12 +16,22 @@ jobs: - project: 'MiniSpace.APIGateway/src/MiniSpace.APIGateway' - project: 'MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Api' test_dir: 'MiniSpace.Services.Identity/tests' + - project: 'MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Api' + test_dir: 'MiniSpace.Services.Posts/tests' + - project: 'MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Api' + test_dir: 'MiniSpace.Services.Comments/tests' + - project: 'MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Api' + test_dir: 'MiniSpace.Services.Organizations/tests' + - project: 'MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Api' + test_dir: 'MiniSpace.Services.Posts/tests' - project: 'MiniSpace.Web/src/MiniSpace.Web' steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 + with: + fetch-depth: 0 - name: Setup .NET 8.0.x - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v2 with: dotnet-version: '8.0.x' @@ -45,5 +59,6 @@ jobs: with: token: ${{ secrets.CODECOV_TOKEN }} files: '**/coverage.cobertura.xml' - slug: SaintAngeLs/p_software_engineering_2 - + slug: SaintAngeLs/distributed_minispace + commit: ${{ github.event.pull_request.base.sha }} + # fail_ci_if_error: true diff --git a/.github/workflows/cloud_deploy.yml b/.github/workflows/cloud_deploy.yml index 5a289e1e..4694a98f 100644 --- a/.github/workflows/cloud_deploy.yml +++ b/.github/workflows/cloud_deploy.yml @@ -3,7 +3,7 @@ name: Deploy to Cloud on: push: branches: - - main # dev for the test purposes here + - main pull_request: branches: - main diff --git a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada-async.docker.yml b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada-async.docker.yml index d5102dde..e1084ef2 100644 --- a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada-async.docker.yml +++ b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada-async.docker.yml @@ -157,6 +157,8 @@ modules: localUrl: localhost:5004 url: identity-service + + reports: path: /reports routes: @@ -189,6 +191,8 @@ modules: localUrl: localhost:5005 url: reports-service + + notifications: path: /notifications routes: @@ -196,19 +200,50 @@ modules: method: POST use: downstream downstream: notifications-service/notifications - auth: true + auth: true - upstream: /{userId} method: GET use: downstream downstream: notifications-service/notifications/{userId} + auth: true + + - upstream: /notification/{notificationId} + method: GET + use: downstream + downstream: notifications-service/notifications/notification/{notificationId} + auth: true + + - upstream: /notification/{userId}/{notificationId} + method: DELETE + use: downstream + downstream: notifications-service/notifications/notification/{userId}/{notificationId} + auth: true + bind: + - userId:{userId} + - notificationId:{notificationId} + + - upstream: /{userId}/{notificationId} + method: GET + use: downstream + downstream: notifications-service/notifications/{userId}/{notificationId} auth: true + description: Retrieves a specific notification for a user by notification ID. + + - upstream: /{userId}/{notificationId}/status + method: PUT + use: downstream + downstream: notifications-service/notifications/{userId}/{notificationId}/status + auth: true + description: Updates the status of a specific notification. services: notifications-service: localUrl: localhost:5006 url: notifications-service + + students: path: /students routes: @@ -266,6 +301,8 @@ modules: localUrl: localhost:5007 url: students-service + + events: path: /events routes: @@ -345,11 +382,31 @@ modules: downstream: events-service/events/organizer/{organizerId} auth: true + - upstream: /{eventId}/participants + method: GET + use: downstream + downstream: events-service/events/{eventId}/participants + auth: true + + - upstream: /{eventId}/participants + method: POST + use: downstream + downstream: events-service/events/{eventId}/participants + auth: true + + - upstream: /{eventId}/participants + method: DELETE + use: downstream + downstream: events-service/events/{eventId}/participants + auth: true + services: events-service: localUrl: localhost:5008 url: events-service + + comments: path: /comments routes: @@ -399,6 +456,8 @@ modules: localUrl: localhost:5009 url: comments-service + + reactions: path: /reactions routes: @@ -429,6 +488,8 @@ modules: localUrl: localhost:5010 url: reactions-service + + statistics: path: /statistics routes: @@ -455,87 +516,104 @@ modules: localUrl: localhost:5011 url: statistics-service + + friends: - path: /friends - routes: - - upstream: / - method: GET - use: downstream - downstream: friends-service/friends - auth: true + path: /friends + routes: + - upstream: / + method: GET + use: downstream + downstream: friends-service/friends + auth: true - - upstream: /{userId} - method: GET - use: downstream - downstream: friends-service/friends/{userId} - bind: - - userId: {userId} - auth: true + - upstream: /{userId} + method: GET + use: downstream + downstream: friends-service/friends/{userId} + bind: + - userId: {userId} + auth: true - - upstream: /{userId}/invite - method: POST - use: downstream - downstream: friends-service/friends/{userId}/invite - bind: - - userId: {userId} - auth: true + - upstream: /{userId}/invite + method: POST + use: downstream + downstream: friends-service/friends/{userId}/invite + bind: + - userId: {userId} + auth: true + afterDispatch: + - use: publish + event: FriendInviteSent + target: notifications-service/events + routingKey: friend_request_created - - upstream: /{requesterId}/{friendId}/remove - method: DELETE - use: downstream - downstream: friends-service/friends/{requesterId}/{friendId}/remove - bind: - - friendId: {friendId} - - requesterId: {requesterId} - auth: true + - upstream: /{requesterId}/{friendId}/remove + method: DELETE + use: downstream + downstream: friends-service/friends/{requesterId}/{friendId}/remove + bind: + - friendId: {friendId} + - requesterId: {requesterId} + auth: true - - upstream: /requests/{userId} - method: GET - use: downstream - downstream: friends-service/friends/requests/{userId} - bind: - - userId: {userId} - auth: true + - upstream: /requests/{userId} + method: GET + use: downstream + downstream: friends-service/friends/requests/{userId} + bind: + - userId: {userId} + auth: true - - upstream: /requests/{userId}/accept - method: POST - use: downstream - downstream: friends-service/friends/requests/{userId}/accept - bind: - - userId: {userId} - auth: true + - upstream: /requests/{userId}/accept + method: POST + use: downstream + downstream: friends-service/friends/requests/{userId}/accept + bind: + - userId: {userId} + auth: true - - upstream: /requests/{userId}/decline - method: POST - use: downstream - downstream: friends-service/friends/requests/{userId}/decline - bind: - - userId: {userId} - auth: true + - upstream: /requests/{userId}/decline + method: POST + use: downstream + downstream: friends-service/friends/requests/{userId}/decline + bind: + - userId: {userId} + auth: true - - upstream: /pending - method: GET - use: downstream - downstream: friends-service/friends/pending - auth: true + - upstream: /pending + method: GET + use: downstream + downstream: friends-service/friends/pending + auth: true - - upstream: /pending/all - method: GET - use: downstream - downstream: friends-service/friends/pending/all - auth: true + - upstream: /pending/all + method: GET + use: downstream + downstream: friends-service/friends/pending/all + auth: true - - upstream: /requests/sent/{userId} - method: GET - use: downstream - downstream: friends-service/friends/requests/sent/{userId} - auth: true + - upstream: /requests/sent/{userId} + method: GET + use: downstream + downstream: friends-service/friends/requests/sent/{userId} + auth: true - services: + - upstream: /requests/{userId}/withdraw + method: PUT + use: downstream + downstream: friends-service/friends/requests/{userId}/withdraw + bind: + - userId: {userId} + auth: true + + services: friends-service: localUrl: localhost:5012 url: friends-service + + posts: path: /posts routes: @@ -544,6 +622,12 @@ modules: use: downstream downstream: posts-service/posts auth: true + + - upstream: /search + method: POST + use: downstream + downstream: posts-service/posts/search + auth: true - upstream: /{postId} method: PUT @@ -562,6 +646,11 @@ modules: - state:{state} auth: true + - upstream: /{postId} + method: GET + use: downstream + downstream: posts-service/posts/{postId} + - upstream: / method: GET use: downstream @@ -573,12 +662,45 @@ modules: downstream: posts-service/posts/{postId} auth: true - services: posts-service: localUrl: localhost:5013 url: posts-service - + + + + mediafiles: + path: /media-files + routes: + - upstream: / + method: POST + use: downstream + downstream: mediafiles-service/media-files + auth: true + + - upstream: /{mediaFileId} + method: GET + use: downstream + downstream: mediafiles-service/media-files/{mediaFileId} + + - upstream: /{mediaFileId}/original + method: GET + use: downstream + downstream: mediafiles-service/media-files/{mediaFileId}/original + + - upstream: /{mediaFileId} + method: DELETE + use: downstream + downstream: mediafiles-service/media-files/{mediaFileId} + auth: true + + services: + mediafiles-service: + localUrl: localhost:5014 + url: mediafiles-service + + + organizations: path: /organizations routes: @@ -587,23 +709,30 @@ modules: use: downstream downstream: organizations-service/organizations auth: true + + - upstream: /{organizationId} + method: DELETE + use: downstream + downstream: organizations-service/organizations/{organizationId} + auth: true + + - upstream: /{organizationId}/children + method: POST + use: downstream + downstream: organizations-service/organizations/{organizationId}/children + auth: true - - upstream: /organizer/{organizationId}/organizer + - upstream: /{organizationId}/organizer method: POST use: downstream downstream: organizations-service/organizations/{organizationId}/organizer auth: true - - upstream: /organizer/{organizationId}/organizer/{organizerId} + - upstream: /{organizationId}/organizer/{organizerId} method: DELETE use: downstream downstream: organizations-service/organizations/{organizationId}/organizer/{organizerId} auth: true - - - upstream: / - method: GET - use: downstream - downstream: organizations-service/organizations - upstream: /{organizationId} method: GET @@ -625,6 +754,11 @@ modules: use: downstream downstream: organizations-service/organizations/{organizationId}/children + - upstream: /{organizationId}/children/all + method: GET + use: downstream + downstream: organizations-service/organizations/{organizationId}/children/all + - upstream: /organizer/{organizerId} method: GET use: downstream diff --git a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada-async.yml b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada-async.yml index 8475d011..7a322b96 100644 --- a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada-async.yml +++ b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada-async.yml @@ -95,6 +95,7 @@ modules: method: GET use: return_value returnValue: Welcome to MiniSpace API [async]! + identity: path: /identity routes: @@ -352,6 +353,24 @@ modules: downstream: events-service/events/organizer/{organizerId} auth: true + - upstream: /{eventId}/participants + method: GET + use: downstream + downstream: events-service/events/{eventId}/participants + auth: true + + - upstream: /{eventId}/participants + method: POST + use: downstream + downstream: events-service/events/{eventId}/participants + auth: true + + - upstream: /{eventId}/participants + method: DELETE + use: downstream + downstream: events-service/events/{eventId}/participants + auth: true + services: events-service: localUrl: localhost:5008 @@ -471,82 +490,95 @@ modules: friends: - path: /friends - routes: - - upstream: / - method: GET - use: downstream - downstream: friends-service/friends - auth: true + path: /friends + routes: + - upstream: / + method: GET + use: downstream + downstream: friends-service/friends + auth: true - - upstream: /{userId} - method: GET - use: downstream - downstream: friends-service/friends/{userId} - bind: - - userId: {userId} - auth: true + - upstream: /{userId} + method: GET + use: downstream + downstream: friends-service/friends/{userId} + bind: + - userId: {userId} + auth: true - - upstream: /{userId}/invite - method: POST - use: downstream - downstream: friends-service/friends/{userId}/invite - bind: - - userId: {userId} - auth: true + - upstream: /{userId}/invite + method: POST + use: downstream + downstream: friends-service/friends/{userId}/invite + bind: + - userId: {userId} + auth: true + afterDispatch: + - use: publish + event: FriendInviteSent + target: notifications-service/events + routingKey: friend_request_created - - upstream: /{requesterId}/{friendId}/remove - method: DELETE - use: downstream - downstream: friends-service/friends/{requesterId}/{friendId}/remove - bind: - - friendId: {friendId} - - requesterId: {requesterId} - auth: true + - upstream: /{requesterId}/{friendId}/remove + method: DELETE + use: downstream + downstream: friends-service/friends/{requesterId}/{friendId}/remove + bind: + - friendId: {friendId} + - requesterId: {requesterId} + auth: true - - upstream: /requests/{userId} - method: GET - use: downstream - downstream: friends-service/friends/requests/{userId} - bind: - - userId: {userId} - auth: true + - upstream: /requests/{userId} + method: GET + use: downstream + downstream: friends-service/friends/requests/{userId} + bind: + - userId: {userId} + auth: true - - upstream: /requests/{userId}/accept - method: POST - use: downstream - downstream: friends-service/friends/requests/{userId}/accept - bind: - - userId: {userId} - auth: true + - upstream: /requests/{userId}/accept + method: POST + use: downstream + downstream: friends-service/friends/requests/{userId}/accept + bind: + - userId: {userId} + auth: true - - upstream: /requests/{userId}/decline - method: POST - use: downstream - downstream: friends-service/friends/requests/{userId}/decline - bind: - - userId: {userId} - auth: true + - upstream: /requests/{userId}/decline + method: POST + use: downstream + downstream: friends-service/friends/requests/{userId}/decline + bind: + - userId: {userId} + auth: true - - upstream: /pending - method: GET - use: downstream - downstream: friends-service/friends/pending - auth: true + - upstream: /pending + method: GET + use: downstream + downstream: friends-service/friends/pending + auth: true - - upstream: /pending/all - method: GET - use: downstream - downstream: friends-service/friends/pending/all - auth: true + - upstream: /pending/all + method: GET + use: downstream + downstream: friends-service/friends/pending/all + auth: true - - upstream: /requests/sent/{userId} - method: GET - use: downstream - downstream: friends-service/friends/requests/sent/{userId} - auth: true + - upstream: /requests/sent/{userId} + method: GET + use: downstream + downstream: friends-service/friends/requests/sent/{userId} + auth: true - services: + - upstream: /requests/{userId}/withdraw + method: PUT + use: downstream + downstream: friends-service/friends/requests/{userId}/withdraw + bind: + - userId: {userId} + auth: true + + services: friends-service: localUrl: localhost:5012 url: friends-service @@ -562,6 +594,12 @@ modules: downstream: posts-service/posts auth: true + - upstream: /search + method: POST + use: downstream + downstream: posts-service/posts/search + auth: true + - upstream: /{postId} method: PUT use: downstream @@ -579,6 +617,11 @@ modules: - state:{state} auth: true + - upstream: /{postId} + method: GET + use: downstream + downstream: posts-service/posts/{postId} + - upstream: / method: GET use: downstream @@ -590,13 +633,44 @@ modules: downstream: posts-service/posts/{postId} auth: true - services: posts-service: localUrl: localhost:5013 url: posts-service + + - + mediafiles: + path: /media-files + routes: + - upstream: / + method: POST + use: downstream + downstream: mediafiles-service/media-files + auth: true + + - upstream: /{mediaFileId} + method: GET + use: downstream + downstream: mediafiles-service/media-files/{mediaFileId} + + - upstream: /{mediaFileId}/original + method: GET + use: downstream + downstream: mediafiles-service/media-files/{mediaFileId}/original + + - upstream: /{mediaFileId} + method: DELETE + use: downstream + downstream: mediafiles-service/media-files/{mediaFileId} + auth: true + + services: + mediafiles-service: + localUrl: localhost:5014 + url: mediafiles-service + + organizations: path: /organizations @@ -606,23 +680,30 @@ modules: use: downstream downstream: organizations-service/organizations auth: true + + - upstream: /{organizationId} + method: DELETE + use: downstream + downstream: organizations-service/organizations/{organizationId} + auth: true + + - upstream: /{organizationId}/children + method: POST + use: downstream + downstream: organizations-service/organizations/{organizationId}/children + auth: true - - upstream: /organizer/{organizationId}/organizer + - upstream: /{organizationId}/organizer method: POST use: downstream downstream: organizations-service/organizations/{organizationId}/organizer auth: true - - upstream: /organizer/{organizationId}/organizer/{organizerId} + - upstream: /{organizationId}/organizer/{organizerId} method: DELETE use: downstream downstream: organizations-service/organizations/{organizationId}/organizer/{organizerId} auth: true - - - upstream: / - method: GET - use: downstream - downstream: organizations-service/organizations - upstream: /{organizationId} method: GET @@ -644,6 +725,11 @@ modules: use: downstream downstream: organizations-service/organizations/{organizationId}/children + - upstream: /{organizationId}/children/all + method: GET + use: downstream + downstream: organizations-service/organizations/{organizationId}/children/all + - upstream: /organizer/{organizerId} method: GET use: downstream diff --git a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.docker.yml b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.docker.yml index 221305f1..22b81f35 100644 --- a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.docker.yml +++ b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.docker.yml @@ -134,6 +134,8 @@ modules: localUrl: localhost:5004 url: identity-service + + reports: path: /reports routes: @@ -166,6 +168,8 @@ modules: localUrl: localhost:5005 url: reports-service + + notifications: path: /notifications routes: @@ -173,19 +177,50 @@ modules: method: POST use: downstream downstream: notifications-service/notifications - auth: true + auth: true - upstream: /{userId} method: GET use: downstream downstream: notifications-service/notifications/{userId} + auth: true + + - upstream: /notification/{notificationId} + method: GET + use: downstream + downstream: notifications-service/notifications/notification/{notificationId} + auth: true + + - upstream: /notification/{userId}/{notificationId} + method: DELETE + use: downstream + downstream: notifications-service/notifications/notification/{userId}/{notificationId} + auth: true + bind: + - userId:{userId} + - notificationId:{notificationId} + + - upstream: /{userId}/{notificationId} + method: GET + use: downstream + downstream: notifications-service/notifications/{userId}/{notificationId} auth: true + description: Retrieves a specific notification for a user by notification ID. + + - upstream: /{userId}/{notificationId}/status + method: PUT + use: downstream + downstream: notifications-service/notifications/{userId}/{notificationId}/status + auth: true + description: Updates the status of a specific notification. services: notifications-service: localUrl: localhost:5006 url: notifications-service + + students: path: /students routes: @@ -243,6 +278,8 @@ modules: localUrl: localhost:5007 url: students-service + + events: path: /events routes: @@ -322,11 +359,31 @@ modules: downstream: events-service/events/organizer/{organizerId} auth: true + - upstream: /{eventId}/participants + method: GET + use: downstream + downstream: events-service/events/{eventId}/participants + auth: true + + - upstream: /{eventId}/participants + method: POST + use: downstream + downstream: events-service/events/{eventId}/participants + auth: true + + - upstream: /{eventId}/participants + method: DELETE + use: downstream + downstream: events-service/events/{eventId}/participants + auth: true + services: events-service: localUrl: localhost:5008 url: events-service + + comments: path: /comments routes: @@ -376,6 +433,8 @@ modules: localUrl: localhost:5009 url: comments-service + + reactions: path: /reactions routes: @@ -406,6 +465,8 @@ modules: localUrl: localhost:5010 url: reactions-service + + statistics: path: /statistics routes: @@ -432,87 +493,104 @@ modules: localUrl: localhost:5011 url: statistics-service + + friends: - path: /friends - routes: - - upstream: / - method: GET - use: downstream - downstream: friends-service/friends - auth: true + path: /friends + routes: + - upstream: / + method: GET + use: downstream + downstream: friends-service/friends + auth: true - - upstream: /{userId} - method: GET - use: downstream - downstream: friends-service/friends/{userId} - bind: - - userId: {userId} - auth: true + - upstream: /{userId} + method: GET + use: downstream + downstream: friends-service/friends/{userId} + bind: + - userId: {userId} + auth: true - - upstream: /{userId}/invite - method: POST - use: downstream - downstream: friends-service/friends/{userId}/invite - bind: - - userId: {userId} - auth: true + - upstream: /{userId}/invite + method: POST + use: downstream + downstream: friends-service/friends/{userId}/invite + bind: + - userId: {userId} + auth: true + afterDispatch: + - use: publish + event: FriendInviteSent + target: notifications-service/events + routingKey: friend_request_created - - upstream: /{requesterId}/{friendId}/remove - method: DELETE - use: downstream - downstream: friends-service/friends/{requesterId}/{friendId}/remove - bind: - - friendId: {friendId} - - requesterId: {requesterId} - auth: true + - upstream: /{requesterId}/{friendId}/remove + method: DELETE + use: downstream + downstream: friends-service/friends/{requesterId}/{friendId}/remove + bind: + - friendId: {friendId} + - requesterId: {requesterId} + auth: true - - upstream: /requests/{userId} - method: GET - use: downstream - downstream: friends-service/friends/requests/{userId} - bind: - - userId: {userId} - auth: true + - upstream: /requests/{userId} + method: GET + use: downstream + downstream: friends-service/friends/requests/{userId} + bind: + - userId: {userId} + auth: true - - upstream: /requests/{userId}/accept - method: POST - use: downstream - downstream: friends-service/friends/requests/{userId}/accept - bind: - - userId: {userId} - auth: true + - upstream: /requests/{userId}/accept + method: POST + use: downstream + downstream: friends-service/friends/requests/{userId}/accept + bind: + - userId: {userId} + auth: true - - upstream: /requests/{userId}/decline - method: POST - use: downstream - downstream: friends-service/friends/requests/{userId}/decline - bind: - - userId: {userId} - auth: true + - upstream: /requests/{userId}/decline + method: POST + use: downstream + downstream: friends-service/friends/requests/{userId}/decline + bind: + - userId: {userId} + auth: true - - upstream: /pending - method: GET - use: downstream - downstream: friends-service/friends/pending - auth: true + - upstream: /pending + method: GET + use: downstream + downstream: friends-service/friends/pending + auth: true - - upstream: /pending/all - method: GET - use: downstream - downstream: friends-service/friends/pending/all - auth: true + - upstream: /pending/all + method: GET + use: downstream + downstream: friends-service/friends/pending/all + auth: true - - upstream: /requests/sent/{userId} - method: GET - use: downstream - downstream: friends-service/friends/requests/sent/{userId} - auth: true + - upstream: /requests/sent/{userId} + method: GET + use: downstream + downstream: friends-service/friends/requests/sent/{userId} + auth: true - services: + - upstream: /requests/{userId}/withdraw + method: PUT + use: downstream + downstream: friends-service/friends/requests/{userId}/withdraw + bind: + - userId: {userId} + auth: true + + services: friends-service: localUrl: localhost:5012 url: friends-service + + posts: path: /posts routes: @@ -522,6 +600,12 @@ modules: downstream: posts-service/posts auth: true + - upstream: /search + method: POST + use: downstream + downstream: posts-service/posts/search + auth: true + - upstream: /{postId} method: PUT use: downstream @@ -555,12 +639,45 @@ modules: downstream: posts-service/posts/{postId} auth: true - services: posts-service: localUrl: localhost:5013 url: posts-service - + + + + mediafiles: + path: /media-files + routes: + - upstream: / + method: POST + use: downstream + downstream: mediafiles-service/media-files + auth: true + + - upstream: /{mediaFileId} + method: GET + use: downstream + downstream: mediafiles-service/media-files/{mediaFileId} + + - upstream: /{mediaFileId}/original + method: GET + use: downstream + downstream: mediafiles-service/media-files/{mediaFileId}/original + + - upstream: /{mediaFileId} + method: DELETE + use: downstream + downstream: mediafiles-service/media-files/{mediaFileId} + auth: true + + services: + mediafiles-service: + localUrl: localhost:5014 + url: mediafiles-service + + + organizations: path: /organizations routes: @@ -569,23 +686,30 @@ modules: use: downstream downstream: organizations-service/organizations auth: true + + - upstream: /{organizationId} + method: DELETE + use: downstream + downstream: organizations-service/organizations/{organizationId} + auth: true + + - upstream: /{organizationId}/children + method: POST + use: downstream + downstream: organizations-service/organizations/{organizationId}/children + auth: true - - upstream: /organizer/{organizationId}/organizer + - upstream: /{organizationId}/organizer method: POST use: downstream downstream: organizations-service/organizations/{organizationId}/organizer auth: true - - upstream: /organizer/{organizationId}/organizer/{organizerId} + - upstream: /{organizationId}/organizer/{organizerId} method: DELETE use: downstream downstream: organizations-service/organizations/{organizationId}/organizer/{organizerId} auth: true - - - upstream: / - method: GET - use: downstream - downstream: organizations-service/organizations - upstream: /{organizationId} method: GET @@ -607,6 +731,11 @@ modules: use: downstream downstream: organizations-service/organizations/{organizationId}/children + - upstream: /{organizationId}/children/all + method: GET + use: downstream + downstream: organizations-service/organizations/{organizationId}/children/all + - upstream: /organizer/{organizerId} method: GET use: downstream diff --git a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.yml b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.yml index 492edb43..7125b6fa 100644 --- a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.yml +++ b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.yml @@ -179,18 +179,52 @@ modules: method: POST use: downstream downstream: notifications-service/notifications - auth: true + auth: true - upstream: /{userId} method: GET use: downstream downstream: notifications-service/notifications/{userId} + auth: true + + - upstream: /notification/{notificationId} + method: GET + use: downstream + downstream: notifications-service/notifications/notification/{notificationId} + auth: true + + - upstream: /notification/{userId}/{notificationId} + method: DELETE + use: downstream + downstream: notifications-service/notifications/notification/{userId}/{notificationId} + auth: true + bind: + - userId:{userId} + - notificationId:{notificationId} + + - upstream: /{userId}/{notificationId} + method: GET + use: downstream + downstream: notifications-service/notifications/{userId}/{notificationId} auth: true + description: Retrieves a specific notification for a user by notification ID. + + - upstream: /{userId}/{notificationId}/status + method: PUT + use: downstream + downstream: notifications-service/notifications/{userId}/{notificationId}/status + auth: true + description: Updates the status of a specific notification. services: notifications-service: localUrl: localhost:5006 url: notifications-service + + + + + @@ -492,6 +526,11 @@ modules: bind: - userId: {userId} auth: true + afterDispatch: + - use: publish + event: FriendInviteSent + target: notifications-service/events + routingKey: friend_request_created - upstream: /{requesterId}/{friendId}/remove method: DELETE @@ -544,6 +583,14 @@ modules: downstream: friends-service/friends/requests/sent/{userId} auth: true + - upstream: /requests/{userId}/withdraw + method: PUT + use: downstream + downstream: friends-service/friends/requests/{userId}/withdraw + bind: + - userId: {userId} + auth: true + services: friends-service: localUrl: localhost:5012 @@ -560,6 +607,12 @@ modules: downstream: posts-service/posts auth: true + - upstream: /search + method: POST + use: downstream + downstream: posts-service/posts/search + auth: true + - upstream: /{postId} method: PUT use: downstream @@ -593,13 +646,44 @@ modules: downstream: posts-service/posts/{postId} auth: true - services: posts-service: localUrl: localhost:5013 url: posts-service + + - + mediafiles: + path: /media-files + routes: + - upstream: / + method: POST + use: downstream + downstream: mediafiles-service/media-files + auth: true + + - upstream: /{mediaFileId} + method: GET + use: downstream + downstream: mediafiles-service/media-files/{mediaFileId} + + - upstream: /{mediaFileId}/original + method: GET + use: downstream + downstream: mediafiles-service/media-files/{mediaFileId}/original + + - upstream: /{mediaFileId} + method: DELETE + use: downstream + downstream: mediafiles-service/media-files/{mediaFileId} + auth: true + + services: + mediafiles-service: + localUrl: localhost:5014 + url: mediafiles-service + + organizations: path: /organizations @@ -609,23 +693,30 @@ modules: use: downstream downstream: organizations-service/organizations auth: true + + - upstream: /{organizationId} + method: DELETE + use: downstream + downstream: organizations-service/organizations/{organizationId} + auth: true + + - upstream: /{organizationId}/children + method: POST + use: downstream + downstream: organizations-service/organizations/{organizationId}/children + auth: true - - upstream: /organizer/{organizationId}/organizer + - upstream: /{organizationId}/organizer method: POST use: downstream downstream: organizations-service/organizations/{organizationId}/organizer auth: true - - upstream: /organizer/{organizationId}/organizer/{organizerId} + - upstream: /{organizationId}/organizer/{organizerId} method: DELETE use: downstream downstream: organizations-service/organizations/{organizationId}/organizer/{organizerId} auth: true - - - upstream: / - method: GET - use: downstream - downstream: organizations-service/organizations - upstream: /{organizationId} method: GET @@ -647,6 +738,11 @@ modules: use: downstream downstream: organizations-service/organizations/{organizationId}/children + - upstream: /{organizationId}/children/all + method: GET + use: downstream + downstream: organizations-service/organizations/{organizationId}/children/all + - upstream: /organizer/{organizerId} method: GET use: downstream @@ -657,5 +753,4 @@ modules: organizations-service: localUrl: localhost:5015 url: organizations-service - - + diff --git a/MiniSpace.Services.Comments/MiniSpace.Services.Comments.sln b/MiniSpace.Services.Comments/MiniSpace.Services.Comments.sln index b474e20c..97759c18 100644 --- a/MiniSpace.Services.Comments/MiniSpace.Services.Comments.sln +++ b/MiniSpace.Services.Comments/MiniSpace.Services.Comments.sln @@ -11,6 +11,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniSpace.Services.Comments EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniSpace.Services.Comments.Infrastructure", "src\MiniSpace.Services.Comments.Infrastructure\MiniSpace.Services.Comments.Infrastructure.csproj", "{85941F19-E28E-467B-A338-7D358D32CFC8}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniSpace.Services.Comments.Application.UnitTests", "tests\MiniSpace.Services.Comments.Application.UnitTests\MiniSpace.Services.Comments.Application.UnitTests.csproj", "{63DF71AF-1D31-4A4D-8127-9B8EB359889D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniSpace.Services.Comments.Core.UnitTests", "tests\MiniSpace.Services.Comments.Core.UnitTests\MiniSpace.Services.Comments.Core.UnitTests.csproj", "{C0DE6BE3-4EE5-4DB5-A170-57351B19AAF8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniSpace.Services.Comments.Infrastructure.UnitTests", "tests\MiniSpace.Services.Comments.Infrastructure.UnitTests\MiniSpace.Services.Comments.Infrastructure.UnitTests.csproj", "{81D7C37A-34BE-43BB-9709-0A866D67F8EB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -33,6 +39,18 @@ Global {85941F19-E28E-467B-A338-7D358D32CFC8}.Debug|Any CPU.Build.0 = Debug|Any CPU {85941F19-E28E-467B-A338-7D358D32CFC8}.Release|Any CPU.ActiveCfg = Release|Any CPU {85941F19-E28E-467B-A338-7D358D32CFC8}.Release|Any CPU.Build.0 = Release|Any CPU + {63DF71AF-1D31-4A4D-8127-9B8EB359889D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {63DF71AF-1D31-4A4D-8127-9B8EB359889D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {63DF71AF-1D31-4A4D-8127-9B8EB359889D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {63DF71AF-1D31-4A4D-8127-9B8EB359889D}.Release|Any CPU.Build.0 = Release|Any CPU + {C0DE6BE3-4EE5-4DB5-A170-57351B19AAF8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C0DE6BE3-4EE5-4DB5-A170-57351B19AAF8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C0DE6BE3-4EE5-4DB5-A170-57351B19AAF8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C0DE6BE3-4EE5-4DB5-A170-57351B19AAF8}.Release|Any CPU.Build.0 = Release|Any CPU + {81D7C37A-34BE-43BB-9709-0A866D67F8EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {81D7C37A-34BE-43BB-9709-0A866D67F8EB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {81D7C37A-34BE-43BB-9709-0A866D67F8EB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {81D7C37A-34BE-43BB-9709-0A866D67F8EB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/Handlers/DeleteLikeHandler.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/Handlers/DeleteLikeHandler.cs index c17a5390..6e64f0e8 100644 --- a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/Handlers/DeleteLikeHandler.cs +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/Handlers/DeleteLikeHandler.cs @@ -34,7 +34,7 @@ public async Task HandleAsync(DeleteLike command, CancellationToken cancellation } var identity = _appContext.Identity; - if (!identity.IsAuthenticated) + if (identity.IsAuthenticated && identity.Id != comment.StudentId) { throw new UnauthorizedCommentAccessException(command.CommentId, identity.Id); } diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Dto/CommentDto.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Dto/CommentDto.cs index 72ca781f..abc5fdc5 100644 --- a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Dto/CommentDto.cs +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Dto/CommentDto.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using MiniSpace.Services.Comments.Core.Entities; namespace MiniSpace.Services.Comments.Application.Dto { + [ExcludeFromCodeCoverage] public class CommentDto { public Guid Id { get; set; } diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Dto/PageableDto.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Dto/PageableDto.cs index 0aa0823b..ab0dd7e8 100644 --- a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Dto/PageableDto.cs +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Dto/PageableDto.cs @@ -1,5 +1,8 @@ -namespace MiniSpace.Services.Comments.Application.Dto +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Comments.Application.Dto { + [ExcludeFromCodeCoverage] public class PageableDto { public int Page { get; set; } diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Dto/SortDto.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Dto/SortDto.cs index 7aaa0fc5..d247ff85 100644 --- a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Dto/SortDto.cs +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Dto/SortDto.cs @@ -1,7 +1,9 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; namespace MiniSpace.Services.Comments.Application.Dto { + [ExcludeFromCodeCoverage] public class SortDto { public IEnumerable SortBy { get; set; } diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/Wrappers/PagedResponse.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/Wrappers/PagedResponse.cs index 8872f116..8dd369a5 100644 --- a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/Wrappers/PagedResponse.cs +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/Wrappers/PagedResponse.cs @@ -1,5 +1,8 @@ -namespace MiniSpace.Services.Comments.Application.Wrappers +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Comments.Application.Wrappers { + [ExcludeFromCodeCoverage] public class PagedResponse : Response { public int TotalPages { get; } diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/Wrappers/Response.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/Wrappers/Response.cs index 016d8ffe..e79bcd9e 100644 --- a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/Wrappers/Response.cs +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/Wrappers/Response.cs @@ -1,5 +1,8 @@ -namespace MiniSpace.Services.Comments.Application.Wrappers +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Comments.Application.Wrappers { + [ExcludeFromCodeCoverage] public class Response { public T Content { get; set; } diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Contexts/IdentityContext.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Contexts/IdentityContext.cs index 12d43b7d..259d5ef2 100644 --- a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Contexts/IdentityContext.cs +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Contexts/IdentityContext.cs @@ -1,4 +1,7 @@ using MiniSpace.Services.Comments.Application; +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("MiniSpace.Services.Comments.Application.UnitTests")] namespace MiniSpace.Services.Comments.Infrastructure.Contexts { diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Decorators/OutboxCommandHandlerDecorator.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Decorators/OutboxCommandHandlerDecorator.cs index 988cdbf3..7bfb24fa 100644 --- a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Decorators/OutboxCommandHandlerDecorator.cs +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Decorators/OutboxCommandHandlerDecorator.cs @@ -2,9 +2,11 @@ using Convey.MessageBrokers; using Convey.MessageBrokers.Outbox; using Convey.Types; +using System.Diagnostics.CodeAnalysis; namespace MiniSpace.Services.Comments.Infrastructure.Decorators { + [ExcludeFromCodeCoverage] [Decorator] internal sealed class OutboxCommandHandlerDecorator : ICommandHandler where TCommand : class, ICommand diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Decorators/OutboxEventHandlerDecorator.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Decorators/OutboxEventHandlerDecorator.cs index 00e34126..f70a293a 100644 --- a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Decorators/OutboxEventHandlerDecorator.cs +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Decorators/OutboxEventHandlerDecorator.cs @@ -2,9 +2,11 @@ using Convey.MessageBrokers; using Convey.MessageBrokers.Outbox; using Convey.Types; +using System.Diagnostics.CodeAnalysis; namespace MiniSpace.Services.Comments.Infrastructure.Decorators { + [ExcludeFromCodeCoverage] [Decorator] internal sealed class OutboxEventHandlerDecorator : IEventHandler where TEvent : class, IEvent diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Exceptions/ExceptionToMessageMapper.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Exceptions/ExceptionToMessageMapper.cs index fdde1c6f..7f5f521f 100644 --- a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Exceptions/ExceptionToMessageMapper.cs +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Exceptions/ExceptionToMessageMapper.cs @@ -3,9 +3,11 @@ using MiniSpace.Services.Comments.Application.Events.Rejected; using MiniSpace.Services.Comments.Application.Exceptions; using MiniSpace.Services.Comments.Core.Exceptions; +using System.Diagnostics.CodeAnalysis; namespace MiniSpace.Services.Comments.Infrastructure.Exceptions { + [ExcludeFromCodeCoverage] internal sealed class ExceptionToMessageMapper : IExceptionToMessageMapper { public object Map(Exception exception, object message) diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Exceptions/ExceptionToResponseMapper.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Exceptions/ExceptionToResponseMapper.cs index cf488338..8c7beb2a 100644 --- a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Exceptions/ExceptionToResponseMapper.cs +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Exceptions/ExceptionToResponseMapper.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; using System.Net; using Convey; using Convey.WebApi.Exceptions; @@ -7,6 +8,7 @@ namespace MiniSpace.Services.Comments.Infrastructure.Exceptions { + [ExcludeFromCodeCoverage] internal sealed class ExceptionToResponseMapper : IExceptionToResponseMapper { private static readonly ConcurrentDictionary Codes = new ConcurrentDictionary(); diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Logging/Extensions.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Logging/Extensions.cs index 777426b5..cab5f9fe 100644 --- a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Logging/Extensions.cs +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Logging/Extensions.cs @@ -2,9 +2,11 @@ using Convey.Logging.CQRS; using Microsoft.Extensions.DependencyInjection; using MiniSpace.Services.Comments.Application.Commands; +using System.Diagnostics.CodeAnalysis; namespace MiniSpace.Services.Comments.Infrastructure.Logging { + [ExcludeFromCodeCoverage] internal static class Extensions { public static IConveyBuilder AddHandlersLogging(this IConveyBuilder builder) diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Logging/MessageToLogTemplateMapper.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Logging/MessageToLogTemplateMapper.cs index 77c86f65..ca36c3c5 100644 --- a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Logging/MessageToLogTemplateMapper.cs +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Logging/MessageToLogTemplateMapper.cs @@ -2,9 +2,11 @@ using MiniSpace.Services.Comments.Application.Commands; using MiniSpace.Services.Comments.Application.Events; using MiniSpace.Services.Comments.Application.Events.External; +using System.Diagnostics.CodeAnalysis; namespace MiniSpace.Services.Comments.Infrastructure.Logging { + [ExcludeFromCodeCoverage] internal sealed class MessageToLogTemplateMapper : IMessageToLogTemplateMapper { private static IReadOnlyDictionary MessageTemplates diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Documents/CommentDocument.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Documents/CommentDocument.cs index d6e3e906..f203397b 100644 --- a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Documents/CommentDocument.cs +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Documents/CommentDocument.cs @@ -1,8 +1,10 @@ using Convey.Types; using MiniSpace.Services.Comments.Core.Entities; +using System.Diagnostics.CodeAnalysis; namespace MiniSpace.Services.Comments.Infrastructure.Mongo.Documents { + [ExcludeFromCodeCoverage] public class CommentDocument : IIdentifiable { public Guid Id { get; set; } diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Documents/Extensions.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Documents/Extensions.cs index 5d02210e..93a578ec 100644 --- a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Documents/Extensions.cs +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Documents/Extensions.cs @@ -1,8 +1,10 @@ using MiniSpace.Services.Comments.Application.Dto; using MiniSpace.Services.Comments.Core.Entities; +using System.Diagnostics.CodeAnalysis; namespace MiniSpace.Services.Comments.Infrastructure.Mongo.Documents { + [ExcludeFromCodeCoverage] public static class Extensions { public static Comment AsEntity(this CommentDocument document) diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Documents/StudentDocument.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Documents/StudentDocument.cs index eea1b15d..65394473 100644 --- a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Documents/StudentDocument.cs +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Documents/StudentDocument.cs @@ -1,7 +1,9 @@ using Convey.Types; +using System.Diagnostics.CodeAnalysis; namespace MiniSpace.Services.Comments.Infrastructure.Mongo.Documents { + [ExcludeFromCodeCoverage] public class StudentDocument : IIdentifiable { public Guid Id { get; set; } diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Repositories/CommentMongoRepository.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Repositories/CommentMongoRepository.cs index 14afb264..158baec2 100644 --- a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Repositories/CommentMongoRepository.cs +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Repositories/CommentMongoRepository.cs @@ -5,9 +5,11 @@ using MiniSpace.Services.Comments.Infrastructure.Mongo.Documents; using MongoDB.Driver; using MongoDB.Driver.Linq; +using System.Diagnostics.CodeAnalysis; namespace MiniSpace.Services.Comments.Infrastructure.Mongo.Repositories { + [ExcludeFromCodeCoverage] public class CommentMongoRepository : ICommentRepository { private readonly IMongoRepository _repository; diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Repositories/Extensions.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Repositories/Extensions.cs index 713d611d..1319102a 100644 --- a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Repositories/Extensions.cs +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Repositories/Extensions.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; @@ -10,6 +11,7 @@ namespace MiniSpace.Services.Comments.Infrastructure.Mongo.Repositories { + [ExcludeFromCodeCoverage] public static class Extensions { private static readonly FilterDefinitionBuilder FilterDefinitionBuilder = Builders.Filter; diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Repositories/StudentMongoRepository.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Repositories/StudentMongoRepository.cs index 6fd9a4fe..d35bc451 100644 --- a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Repositories/StudentMongoRepository.cs +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Repositories/StudentMongoRepository.cs @@ -2,9 +2,11 @@ using MiniSpace.Services.Comments.Core.Entities; using MiniSpace.Services.Comments.Core.Repositories; using MiniSpace.Services.Comments.Infrastructure.Mongo.Documents; +using System.Diagnostics.CodeAnalysis; namespace MiniSpace.Services.Comments.Infrastructure.Mongo.Repositories { + [ExcludeFromCodeCoverage] public class StudentMongoRepository : IStudentRepository { private readonly IMongoRepository _repository; diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Services/MessageBroker.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Services/MessageBroker.cs index c4355c2a..935d792e 100644 --- a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Services/MessageBroker.cs +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Services/MessageBroker.cs @@ -6,7 +6,9 @@ using Microsoft.Extensions.Logging; using OpenTracing; using MiniSpace.Services.Comments.Application.Services; +using System.Runtime.CompilerServices; +[assembly: InternalsVisibleTo("MiniSpace.Services.Comments.Infrastructure.UnitTests")] namespace MiniSpace.Services.Comments.Infrastructure.Services { internal sealed class MessageBroker : IMessageBroker diff --git a/MiniSpace.Services.Comments/tests/MiniSpace.Services.Comments.Application.UnitTests/Commands/Handlers/AddLikeHandlerTest.cs b/MiniSpace.Services.Comments/tests/MiniSpace.Services.Comments.Application.UnitTests/Commands/Handlers/AddLikeHandlerTest.cs new file mode 100644 index 00000000..6a196b5d --- /dev/null +++ b/MiniSpace.Services.Comments/tests/MiniSpace.Services.Comments.Application.UnitTests/Commands/Handlers/AddLikeHandlerTest.cs @@ -0,0 +1,92 @@ +using Xunit; +using Moq; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Comments.Application.Exceptions; +using MiniSpace.Services.Comments.Application.Services; +using MiniSpace.Services.Comments.Core.Entities; +using MiniSpace.Services.Comments.Core.Repositories; +using MiniSpace.Services.Comments.Application.Commands.Handlers; +using MiniSpace.Services.Comments.Application.Commands; +using MiniSpace.Services.Comments.Infrastructure.Contexts; +using System.Threading; +using FluentAssertions; + +namespace MiniSpace.Services.Comments.Application.UnitTests.Commands.Handlers +{ + public class AddLikeHandlerTest + { + private readonly AddLikeHandler _addLikeHandler; + private readonly Mock _commentRepositoryMock; + private readonly Mock _messageBrokerMock; + private readonly Mock _appContextMock; + + public AddLikeHandlerTest() + { + _commentRepositoryMock = new Mock(); + _messageBrokerMock = new Mock(); + _appContextMock = new Mock(); + _addLikeHandler = new AddLikeHandler(_commentRepositoryMock.Object, _appContextMock.Object, _messageBrokerMock.Object); + } + + [Fact] + public async Task HandleAsync_WithValidCommentAndAuthorised_ShouldAddLike() + { + // Arrange + var commentId = Guid.NewGuid(); + var comand = new AddLike(commentId); + + var comment = Comment.Create(new AggregateId(commentId), Guid.NewGuid(), CommentContext.Post, Guid.NewGuid(), "Adam", Guid.NewGuid(), "text", DateTime.Now); + + var identityContext = new IdentityContext(Guid.NewGuid().ToString(), "", true, new Dictionary()); + + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + _commentRepositoryMock.Setup(repo => repo.GetAsync(comment.Id)).ReturnsAsync(comment); + + var cancelationToken = new CancellationToken(); + + // Act + await _addLikeHandler.HandleAsync(comand, cancelationToken); + + // Assert + _commentRepositoryMock.Verify(repo => repo.UpdateAsync(comment), Times.Once()); + } + + [Fact] + public async Task HandleAsync_WithInvalidComment_ShouldThrowCommentNotFoundExeption() + { + // Arrange + var commentId = Guid.NewGuid(); + var comand = new AddLike(commentId); + _commentRepositoryMock.Setup(repo => repo.GetAsync(comand.CommentId)).ReturnsAsync((Comment)null); + var cancelationToken = new CancellationToken(); + + // Act & Assert + Func act = async () => await _addLikeHandler.HandleAsync(comand, cancelationToken); + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task HandleAsync_WithNonPermitedIdentity_ShouldThrowUnauthorizedCommentAccessException() + { + // Arrange + var commentId = Guid.NewGuid(); + var comand = new AddLike(commentId); + + var comment = Comment.Create(new AggregateId(commentId), Guid.NewGuid(), CommentContext.Post, Guid.NewGuid(), "Adam", Guid.NewGuid(), "text", DateTime.Now); + + var identityContext = new IdentityContext(Guid.NewGuid().ToString(), "", false, new Dictionary()); + + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + _commentRepositoryMock.Setup(repo => repo.GetAsync(comment.Id)).ReturnsAsync(comment); + + var cancelationToken = new CancellationToken(); + + // Act & Assert + Func act = async () => await _addLikeHandler.HandleAsync(comand, cancelationToken); + await act.Should().ThrowAsync(); + + } + } +} diff --git a/MiniSpace.Services.Comments/tests/MiniSpace.Services.Comments.Application.UnitTests/Commands/Handlers/CreateCommentHandlerTest.cs b/MiniSpace.Services.Comments/tests/MiniSpace.Services.Comments.Application.UnitTests/Commands/Handlers/CreateCommentHandlerTest.cs new file mode 100644 index 00000000..d24036a6 --- /dev/null +++ b/MiniSpace.Services.Comments/tests/MiniSpace.Services.Comments.Application.UnitTests/Commands/Handlers/CreateCommentHandlerTest.cs @@ -0,0 +1,231 @@ +using Xunit; +using Moq; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Linq; +using System.Text; +using MiniSpace.Services.Comments.Application.Events; +using MiniSpace.Services.Comments.Application.Exceptions; +using MiniSpace.Services.Comments.Application.Services; +using MiniSpace.Services.Comments.Core.Entities; +using MiniSpace.Services.Comments.Core.Repositories; +using MiniSpace.Services.Comments.Application.Commands.Handlers; +using MiniSpace.Services.Comments.Application.Commands; +using MiniSpace.Services.Comments.Infrastructure.Contexts; +using Convey.CQRS.Commands; +using System.Threading; +using System.Security.Claims; +using FluentAssertions; + +namespace MiniSpace.Services.Comments.Application.UnitTests.Commands.Handlers +{ + public class CreateCommentHandlerTest + { + private readonly CreateCommentHandler _createCommentHandler; + private readonly Mock _commentRepositoryMock; + private readonly Mock _studentRepositoryMock; + private readonly Mock _messageBrokerMock; + private readonly Mock _eventMapperMock; + private readonly Mock _appContextMock; + private readonly Mock _dateTimeProviderMock; + + public CreateCommentHandlerTest() + { + _commentRepositoryMock = new Mock(); + _studentRepositoryMock = new Mock(); + _messageBrokerMock = new Mock(); + _eventMapperMock = new Mock(); + _appContextMock = new Mock(); + _dateTimeProviderMock = new Mock(); + _createCommentHandler = new CreateCommentHandler( + _commentRepositoryMock.Object, + _studentRepositoryMock.Object, + _dateTimeProviderMock.Object, + _messageBrokerMock.Object, + _appContextMock.Object + ); + } + + [Fact] + public async Task HandleAsync_WithVaildStudentAndContextNoParent_ShouldNotThrowExeption() + { + // Arrange + var commentId = Guid.NewGuid(); + var contextId = Guid.NewGuid(); + var studentId = Guid.NewGuid(); + var parentId = Guid.Empty; + var command = new CreateComment(commentId, contextId, "Post", studentId, + parentId, "text"); + var cancelationTocken = new CancellationToken(); + + var identityContext = new IdentityContext(studentId.ToString(), "", + true, new Dictionary()); + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + + _studentRepositoryMock.Setup(repo => repo.ExistsAsync(studentId)) + .ReturnsAsync(true); + + // Act & Assert + Func act = async () => await _createCommentHandler.HandleAsync(command, cancelationTocken); + await act.Should().NotThrowAsync(); + } + + [Fact] + public async Task HandleAsync_WithVaildStudentAndContextandOneParent_ShouldNotThrowExeptionAndUpdateParent() + { + // Arrange + var commentId = Guid.NewGuid(); + var contextId = Guid.NewGuid(); + var studentId = Guid.NewGuid(); + var parentId = Guid.NewGuid(); + var parentComment = Comment.Create(new AggregateId(parentId), contextId, + CommentContext.Post, Guid.NewGuid(), "alex", Guid.Empty, "text", DateTime.Now); + var command = new CreateComment(commentId, contextId, "Post", studentId, + parentId, "text"); + var cancelationTocken = new CancellationToken(); + + var identityContext = new IdentityContext(studentId.ToString(), "", + true, new Dictionary()); + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + + _studentRepositoryMock.Setup(repo => repo.ExistsAsync(studentId)) + .ReturnsAsync(true); + _commentRepositoryMock.Setup(repo => repo.GetAsync(parentId)) + .ReturnsAsync(parentComment); + + // Act & Assert + Func act = async () => await _createCommentHandler.HandleAsync(command, cancelationTocken); + await act.Should().NotThrowAsync(); + _commentRepositoryMock.Verify(repo => repo.UpdateAsync(parentComment), Times.Once()); + } + + [Fact] + public async Task HandleAsync_WithNonPermitedIdentity_ShouldThrowUnauthorizedCommentAccessException() + { + // Arrange + var commentId = Guid.NewGuid(); + var contextId = Guid.NewGuid(); + var studentId = Guid.NewGuid(); + var parentId = Guid.Empty; + var command = new CreateComment(commentId, contextId, "Post", studentId, + parentId, "text"); + var cancelationTocken = new CancellationToken(); + + var identityContext = new IdentityContext(Guid.NewGuid().ToString(), "", + true, new Dictionary()); + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + + _studentRepositoryMock.Setup(repo => repo.ExistsAsync(studentId)) + .ReturnsAsync(true); + + // Act & Assert + Func act = async () => await _createCommentHandler.HandleAsync(command, cancelationTocken); + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task HandleAsync_WithWrongStudentId_ShouldThrowStudentNotFoundException() + { + // Arrange + var commentId = Guid.NewGuid(); + var contextId = Guid.NewGuid(); + var studentId = Guid.NewGuid(); + var parentId = Guid.Empty; + var command = new CreateComment(commentId, contextId, "Post", studentId, + parentId, "text"); + var cancelationTocken = new CancellationToken(); + + var identityContext = new IdentityContext(studentId.ToString(), "", + false, new Dictionary()); + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + + _studentRepositoryMock.Setup(repo => repo.ExistsAsync(studentId)) + .ReturnsAsync(false); + + // Act & Assert + Func act = async () => await _createCommentHandler.HandleAsync(command, cancelationTocken); + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task HandleAsync_WithWrongCommentContextEnum_ShouldThrowInvalidCommentContextEnumException() + { + // Arrange + var commentId = Guid.NewGuid(); + var contextId = Guid.NewGuid(); + var studentId = Guid.NewGuid(); + var parentId = Guid.Empty; + var command = new CreateComment(commentId, contextId, "wrongenumstring", studentId, + parentId, "text"); + var cancelationTocken = new CancellationToken(); + + var identityContext = new IdentityContext(studentId.ToString(), "", + false, new Dictionary()); + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + + _studentRepositoryMock.Setup(repo => repo.ExistsAsync(studentId)) + .ReturnsAsync(true); + + // Act & Assert + Func act = async () => await _createCommentHandler.HandleAsync(command, cancelationTocken); + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task HandleAsync_WithWrongParentId_ShouldThrowParentCommentNotFoundException() + { + // Arrange + var commentId = Guid.NewGuid(); + var contextId = Guid.NewGuid(); + var studentId = Guid.NewGuid(); + var parentId = Guid.NewGuid(); + var parentComment = Comment.Create(new AggregateId(parentId), contextId, + CommentContext.Post, Guid.NewGuid(), "alex", Guid.Empty, "text", DateTime.Now); + var command = new CreateComment(commentId, contextId, "Post", studentId, + parentId, "text"); + var cancelationTocken = new CancellationToken(); + + var identityContext = new IdentityContext(studentId.ToString(), "", + false, new Dictionary()); + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + + _studentRepositoryMock.Setup(repo => repo.ExistsAsync(studentId)) + .ReturnsAsync(true); + _commentRepositoryMock.Setup(repo => repo.GetAsync(parentId)) + .ReturnsAsync((Comment)null); + + // Act & Assert + Func act = async () => await _createCommentHandler.HandleAsync(command, cancelationTocken); + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task HandleAsync_WithParentThatHasAParent_ShouldThrowInvalidParentCommentException() + { + // Arrange + var commentId = Guid.NewGuid(); + var contextId = Guid.NewGuid(); + var studentId = Guid.NewGuid(); + var parentId = Guid.NewGuid(); + var parentComment = Comment.Create(new AggregateId(parentId), contextId, + CommentContext.Post, Guid.NewGuid(), "alex", Guid.NewGuid(), "text", DateTime.Now); + var command = new CreateComment(commentId, contextId, "Post", studentId, + parentId, "text"); + var cancelationTocken = new CancellationToken(); + + var identityContext = new IdentityContext(studentId.ToString(), "", + true, new Dictionary()); + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + + _studentRepositoryMock.Setup(repo => repo.ExistsAsync(studentId)) + .ReturnsAsync(true); + _commentRepositoryMock.Setup(repo => repo.GetAsync(parentId)) + .ReturnsAsync(parentComment); + + // Act & Assert + Func act = async () => await _createCommentHandler.HandleAsync(command, cancelationTocken); + await act.Should().ThrowAsync(); + } + } +} diff --git a/MiniSpace.Services.Comments/tests/MiniSpace.Services.Comments.Application.UnitTests/Commands/Handlers/DeleteCommentHandlerTest.cs b/MiniSpace.Services.Comments/tests/MiniSpace.Services.Comments.Application.UnitTests/Commands/Handlers/DeleteCommentHandlerTest.cs new file mode 100644 index 00000000..82333165 --- /dev/null +++ b/MiniSpace.Services.Comments/tests/MiniSpace.Services.Comments.Application.UnitTests/Commands/Handlers/DeleteCommentHandlerTest.cs @@ -0,0 +1,141 @@ +using Xunit; +using Moq; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Comments.Application.Exceptions; +using MiniSpace.Services.Comments.Application.Services; +using MiniSpace.Services.Comments.Core.Entities; +using MiniSpace.Services.Comments.Core.Repositories; +using MiniSpace.Services.Comments.Application.Commands.Handlers; +using MiniSpace.Services.Comments.Application.Commands; +using MiniSpace.Services.Comments.Infrastructure.Contexts; +using System.Threading; +using FluentAssertions; + +namespace MiniSpace.Services.Comments.Application.UnitTests.Commands.Handlers +{ + public class DeleteCommentHandlerTest + { + private readonly DeleteCommentHandler _deleteCommentHandler; + private readonly Mock _commentRepositoryMock; + private readonly Mock _messageBrokerMock; + private readonly Mock _appContextMock; + + public DeleteCommentHandlerTest() + { + _commentRepositoryMock = new Mock(); + _messageBrokerMock = new Mock(); + _appContextMock = new Mock(); + _deleteCommentHandler = new DeleteCommentHandler( + _commentRepositoryMock.Object, + _appContextMock.Object, + _messageBrokerMock.Object + ); + } + + [Fact] + public async Task HandleAsync_WithVaildStudentAndPermited_ShouldUpdateRepository() + { + // Arrange + var commentId = Guid.NewGuid(); + var comand = new DeleteComment(commentId); + var studentId = Guid.NewGuid(); + + var comment = Comment.Create(new AggregateId(commentId), Guid.NewGuid(), + CommentContext.Post, studentId, "Adam", Guid.NewGuid(), "text", DateTime.Now); + + var identityContext = new IdentityContext(studentId.ToString(), + "", true, new Dictionary()); + + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + _commentRepositoryMock.Setup(repo => repo.GetAsync(comment.Id)) + .ReturnsAsync(comment); + + var cancelationToken = new CancellationToken(); + + // Act + await _deleteCommentHandler.HandleAsync(comand, cancelationToken); + + // Assert + _commentRepositoryMock.Verify(repo => repo.UpdateAsync(comment), Times.Once()); + } + + [Fact] + public async Task HandleAsync_WithInvalidComment_ShouldThrowCommentNotFoundExeption() + { + // Arrange + var commentId = Guid.NewGuid(); + var comand = new DeleteComment(commentId); + var studentId = Guid.NewGuid(); + + var comment = Comment.Create(new AggregateId(commentId), Guid.NewGuid(), + CommentContext.Post, studentId, "Adam", Guid.NewGuid(), "text", DateTime.Now); + + var identityContext = new IdentityContext(studentId.ToString(), + "", true, new Dictionary()); + + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + _commentRepositoryMock.Setup(repo => repo.GetAsync(comment.Id)) + .ReturnsAsync((Comment)null); + + var cancelationToken = new CancellationToken(); + + // Act & Assert + Func act = async () => await _deleteCommentHandler.HandleAsync(comand, cancelationToken); + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task HandleAsync_WithAdminNotTheirs_ShouldNotThrowException() + { + // Arrange + var commentId = Guid.NewGuid(); + var comand = new DeleteComment(commentId); + var studentId = Guid.NewGuid(); + + var comment = Comment.Create(new AggregateId(commentId), Guid.NewGuid(), + CommentContext.Post, studentId, "Adam", Guid.NewGuid(), "text", DateTime.Now); + + var identityContext = new IdentityContext(Guid.NewGuid().ToString(), + "Admin", true, new Dictionary()); + + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + _commentRepositoryMock.Setup(repo => repo.GetAsync(comment.Id)) + .ReturnsAsync(comment); + + var cancelationToken = new CancellationToken(); + + // Act & Assert + Func act = async () => await _deleteCommentHandler.HandleAsync(comand, cancelationToken); + await act.Should().NotThrowAsync(); + + } + + [Fact] + public async Task HandleAsync_WithNotAdminNotTheirs_ShouldThrowUnauthorizedCommentAccessException() + { + // Arrange + var commentId = Guid.NewGuid(); + var comand = new DeleteComment(commentId); + var studentId = Guid.NewGuid(); + + var comment = Comment.Create(new AggregateId(commentId), Guid.NewGuid(), + CommentContext.Post, studentId, "Adam", Guid.NewGuid(), "text", DateTime.Now); + + var identityContext = new IdentityContext(Guid.NewGuid().ToString(), + "", true, new Dictionary()); + + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + _commentRepositoryMock.Setup(repo => repo.GetAsync(comment.Id)) + .ReturnsAsync(comment); + + var cancelationToken = new CancellationToken(); + + // Act & Assert + Func act = async () => await _deleteCommentHandler.HandleAsync(comand, cancelationToken); + await act.Should().ThrowAsync(); + + } + } +} diff --git a/MiniSpace.Services.Comments/tests/MiniSpace.Services.Comments.Application.UnitTests/Commands/Handlers/DeleteLikeHandlerTest.cs b/MiniSpace.Services.Comments/tests/MiniSpace.Services.Comments.Application.UnitTests/Commands/Handlers/DeleteLikeHandlerTest.cs new file mode 100644 index 00000000..f61c2da2 --- /dev/null +++ b/MiniSpace.Services.Comments/tests/MiniSpace.Services.Comments.Application.UnitTests/Commands/Handlers/DeleteLikeHandlerTest.cs @@ -0,0 +1,116 @@ +using Xunit; +using Moq; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Comments.Application.Exceptions; +using MiniSpace.Services.Comments.Application.Services; +using MiniSpace.Services.Comments.Core.Entities; +using MiniSpace.Services.Comments.Core.Repositories; +using MiniSpace.Services.Comments.Application.Commands.Handlers; +using MiniSpace.Services.Comments.Application.Commands; +using MiniSpace.Services.Comments.Infrastructure.Contexts; +using System.Threading; +using FluentAssertions; + +namespace MiniSpace.Services.Comments.Application.UnitTests.Commands.Handlers +{ + public class DeleteLikeHandlerTest + { + private readonly DeleteLikeHandler _deleteLikeHandler; + private readonly Mock _commentRepositoryMock; + private readonly Mock _messageBrokerMock; + private readonly Mock _appContextMock; + + public DeleteLikeHandlerTest() + { + _commentRepositoryMock = new Mock(); + _messageBrokerMock = new Mock(); + _appContextMock = new Mock(); + _deleteLikeHandler = new DeleteLikeHandler( + _commentRepositoryMock.Object, + _appContextMock.Object, + _messageBrokerMock.Object + ); + } + + [Fact] + public async Task HandleAsync_WithVaildStudentAndPermited_ShouldUpdateRepository() + { + // Arrange + var commentId = Guid.NewGuid(); + var comand = new DeleteLike(commentId); + var studentId = Guid.NewGuid(); + + var comment = Comment.Create(new AggregateId(commentId), Guid.NewGuid(), + CommentContext.Post, studentId, "Adam", Guid.NewGuid(), "text", DateTime.Now); + comment.Like(studentId); + + var identityContext = new IdentityContext(studentId.ToString(), + "", true, new Dictionary()); + + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + _commentRepositoryMock.Setup(repo => repo.GetAsync(comment.Id)) + .ReturnsAsync(comment); + + var cancelationToken = new CancellationToken(); + + // Act + await _deleteLikeHandler.HandleAsync(comand, cancelationToken); + + // Assert + _commentRepositoryMock.Verify(repo => repo.UpdateAsync(comment), Times.Once()); + } + + [Fact] + public async Task HandleAsync_WithInvalidComment_ShouldThrowCommentNotFoundExeption() + { + // Arrange + var commentId = Guid.NewGuid(); + var comand = new DeleteLike(commentId); + var studentId = Guid.NewGuid(); + + var comment = Comment.Create(new AggregateId(commentId), Guid.NewGuid(), + CommentContext.Post, studentId, "Adam", Guid.NewGuid(), "text", DateTime.Now); + + var identityContext = new IdentityContext(studentId.ToString(), + "", true, new Dictionary()); + + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + _commentRepositoryMock.Setup(repo => repo.GetAsync(comment.Id)) + .ReturnsAsync((Comment)null); + + var cancelationToken = new CancellationToken(); + + // Act & Assert + Func act = async () => await _deleteLikeHandler.HandleAsync(comand, cancelationToken); + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task HandleAsync_WithNotTheirs_ShouldThrowUnauthorizedCommentAccessException() + { + // Arrange + var commentId = Guid.NewGuid(); + var comand = new DeleteLike(commentId); + var studentId = Guid.NewGuid(); + + var comment = Comment.Create(new AggregateId(commentId), Guid.NewGuid(), + CommentContext.Post, studentId, "Adam", Guid.NewGuid(), "text", DateTime.Now); + + var identityContext = new IdentityContext(Guid.NewGuid().ToString(), + "", true, new Dictionary()); + + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + _commentRepositoryMock.Setup(repo => repo.GetAsync(comment.Id)) + .ReturnsAsync(comment); + + var cancelationToken = new CancellationToken(); + + // Act & Assert + Func act = async () => await _deleteLikeHandler.HandleAsync(comand, cancelationToken); + await act.Should().ThrowAsync(); + + } + } +} diff --git a/MiniSpace.Services.Comments/tests/MiniSpace.Services.Comments.Application.UnitTests/Commands/Handlers/UpdateCommnetHandlerTest.cs b/MiniSpace.Services.Comments/tests/MiniSpace.Services.Comments.Application.UnitTests/Commands/Handlers/UpdateCommnetHandlerTest.cs new file mode 100644 index 00000000..e96d8cb6 --- /dev/null +++ b/MiniSpace.Services.Comments/tests/MiniSpace.Services.Comments.Application.UnitTests/Commands/Handlers/UpdateCommnetHandlerTest.cs @@ -0,0 +1,156 @@ +using Xunit; +using Moq; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Comments.Application.Exceptions; +using MiniSpace.Services.Comments.Application.Services; +using MiniSpace.Services.Comments.Core.Entities; +using MiniSpace.Services.Comments.Core.Repositories; +using MiniSpace.Services.Comments.Application.Commands.Handlers; +using MiniSpace.Services.Comments.Application.Commands; +using MiniSpace.Services.Comments.Infrastructure.Contexts; +using System.Threading; +using FluentAssertions; + +namespace MiniSpace.Services.Comments.Application.UnitTests.Commands.Handlers +{ + public class UpdateCommnetHandlerTest + { + public readonly UpdateCommentHandler _updateCommentHandler; + private readonly Mock _commentRepositoryMock; + private readonly Mock _messageBrokerMock; + private readonly Mock _appContextMock; + private readonly Mock _dateTimeProviderMock; + + public UpdateCommnetHandlerTest() + { + _commentRepositoryMock = new Mock(); + _messageBrokerMock = new Mock(); + _appContextMock = new Mock(); + _dateTimeProviderMock = new Mock(); + _updateCommentHandler = new UpdateCommentHandler( + _commentRepositoryMock.Object, + _appContextMock.Object, + _messageBrokerMock.Object, + _dateTimeProviderMock.Object + ); + } + + [Fact] + public async Task HandleAsync_WithVaildStudentAndPermited_ShouldUpdateRepository() + { + // Arrange + var commentId = Guid.NewGuid(); + var comand = new UpdateComment(commentId, "newText"); + var studentId = Guid.NewGuid(); + + var comment = Comment.Create(new AggregateId(commentId), Guid.NewGuid(), + CommentContext.Post, studentId, "Adam", Guid.NewGuid(), "text", DateTime.Now); + + var identityContext = new IdentityContext(studentId.ToString(), + "", true, new Dictionary()); + + var dateTimeNow = DateTime.Now; + + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + _commentRepositoryMock.Setup(repo => repo.GetAsync(comment.Id)) + .ReturnsAsync(comment); + _dateTimeProviderMock.Setup(dtp => dtp.Now).Returns(dateTimeNow); + + var cancelationToken = new CancellationToken(); + + // Act + await _updateCommentHandler.HandleAsync(comand, cancelationToken); + + // Assert + _commentRepositoryMock.Verify(repo => repo.UpdateAsync(comment), Times.Once()); + } + + [Fact] + public async Task HandleAsync_WithInvalidComment_ShouldThrowCommentNotFoundExeption() + { + // Arrange + var commentId = Guid.NewGuid(); + var comand = new UpdateComment(commentId, "newText"); + var studentId = Guid.NewGuid(); + + var comment = Comment.Create(new AggregateId(commentId), Guid.NewGuid(), + CommentContext.Post, studentId, "Adam", Guid.NewGuid(), "text", DateTime.Now); + + var identityContext = new IdentityContext(studentId.ToString(), + "", true, new Dictionary()); + + var dateTimeNow = DateTime.Now; + + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + _commentRepositoryMock.Setup(repo => repo.GetAsync(comment.Id)) + .ReturnsAsync((Comment)null); + _dateTimeProviderMock.Setup(dtp => dtp.Now).Returns(dateTimeNow); + + var cancelationToken = new CancellationToken(); + + // Act & Assert + Func act = async () => await _updateCommentHandler.HandleAsync(comand, cancelationToken); + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task HandleAsync_WithAdminNotTheirs_ShouldNotThrowException() + { + // Arrange + var commentId = Guid.NewGuid(); + var comand = new UpdateComment(commentId, "newText"); + var studentId = Guid.NewGuid(); + + var comment = Comment.Create(new AggregateId(commentId), Guid.NewGuid(), + CommentContext.Post, studentId, "Adam", Guid.NewGuid(), "text", DateTime.Now); + + var identityContext = new IdentityContext(Guid.NewGuid().ToString(), + "Admin", true, new Dictionary()); + + var dateTimeNow = DateTime.Now; + + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + _commentRepositoryMock.Setup(repo => repo.GetAsync(comment.Id)) + .ReturnsAsync(comment); + _dateTimeProviderMock.Setup(dtp => dtp.Now).Returns(dateTimeNow); + + var cancelationToken = new CancellationToken(); + + // Act & Assert + Func act = async () => await _updateCommentHandler.HandleAsync(comand, cancelationToken); + await act.Should().NotThrowAsync(); + + } + + [Fact] + public async Task HandleAsync_WithNotAdminNotTheirs_ShouldThrowUnauthorizedCommentAccessException() + { + // Arrange + var commentId = Guid.NewGuid(); + var comand = new UpdateComment(commentId, "newText"); + var studentId = Guid.NewGuid(); + + var comment = Comment.Create(new AggregateId(commentId), Guid.NewGuid(), + CommentContext.Post, studentId, "Adam", Guid.NewGuid(), "text", DateTime.Now); + + var identityContext = new IdentityContext(Guid.NewGuid().ToString(), + "", true, new Dictionary()); + + var dateTimeNow = DateTime.Now; + + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + _commentRepositoryMock.Setup(repo => repo.GetAsync(comment.Id)) + .ReturnsAsync(comment); + _dateTimeProviderMock.Setup(dtp => dtp.Now).Returns(dateTimeNow); + + var cancelationToken = new CancellationToken(); + + // Act & Assert + Func act = async () => await _updateCommentHandler.HandleAsync(comand, cancelationToken); + await act.Should().ThrowAsync(); + + } + } +} diff --git a/MiniSpace.Services.Comments/tests/MiniSpace.Services.Comments.Application.UnitTests/Events/External/Handlers/EventDeletedHandlerTest.cs b/MiniSpace.Services.Comments/tests/MiniSpace.Services.Comments.Application.UnitTests/Events/External/Handlers/EventDeletedHandlerTest.cs new file mode 100644 index 00000000..f78a7cfc --- /dev/null +++ b/MiniSpace.Services.Comments/tests/MiniSpace.Services.Comments.Application.UnitTests/Events/External/Handlers/EventDeletedHandlerTest.cs @@ -0,0 +1,59 @@ +using Xunit; +using Moq; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Comments.Application.Exceptions; +using MiniSpace.Services.Comments.Application.Services; +using MiniSpace.Services.Comments.Core.Entities; +using MiniSpace.Services.Comments.Core.Repositories; +using MiniSpace.Services.Comments.Application.Commands.Handlers; +using MiniSpace.Services.Comments.Application.Commands; +using MiniSpace.Services.Comments.Infrastructure.Contexts; +using System.Threading; +using FluentAssertions; +using MiniSpace.Services.Comments.Application.Events.External.Handlers; +using MiniSpace.Services.Comments.Application.Events.External; +using System.ComponentModel.Design; + +namespace MiniSpace.Services.Comments.Application.UnitTests.Events.External.Handlers +{ + public class EventDeletedHandlerTest + { + private readonly EventDeletedHandler _eventDeletedHandler; + private readonly Mock _commentRepositoryMock; + + public EventDeletedHandlerTest() + { + _commentRepositoryMock = new Mock(); + _eventDeletedHandler = new EventDeletedHandler(_commentRepositoryMock.Object); + } + + [Fact] + public async Task HandleAsync_ValidData_ShouldNotThrowExeption() + { + // Arrange + var eventId = System.Guid.NewGuid(); + var @event = new EventDeleted(eventId); + var commentId1 = System.Guid.NewGuid(); + var commentId2 = System.Guid.NewGuid(); + var comment1 = Comment.Create(new AggregateId(commentId1), eventId, + CommentContext.Event, System.Guid.NewGuid(), "Adam", System.Guid.NewGuid(), "text", DateTime.Now); + var comment2 = Comment.Create(new AggregateId(commentId2), eventId, + CommentContext.Event, Guid.NewGuid(), "Bartek", Guid.NewGuid(), "text", DateTime.Now); + var commentsList = new List { comment1, comment2 }; + + _commentRepositoryMock.Setup(repo => repo.GetByEventIdAsync(eventId)) + .ReturnsAsync(commentsList); + + var cancelationTocken = new CancellationToken(); + + // Act + await _eventDeletedHandler.HandleAsync(@event, cancelationTocken); + + // Assert + _commentRepositoryMock.Verify(repo => repo.DeleteAsync(commentId1), Times.Once()); + _commentRepositoryMock.Verify(repo => repo.DeleteAsync(commentId2), Times.Once()); + } + } +} diff --git a/MiniSpace.Services.Comments/tests/MiniSpace.Services.Comments.Application.UnitTests/Events/External/Handlers/PostDeletedHandlerTest.cs b/MiniSpace.Services.Comments/tests/MiniSpace.Services.Comments.Application.UnitTests/Events/External/Handlers/PostDeletedHandlerTest.cs new file mode 100644 index 00000000..6e1569f7 --- /dev/null +++ b/MiniSpace.Services.Comments/tests/MiniSpace.Services.Comments.Application.UnitTests/Events/External/Handlers/PostDeletedHandlerTest.cs @@ -0,0 +1,59 @@ +using Xunit; +using Moq; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Comments.Application.Exceptions; +using MiniSpace.Services.Comments.Application.Services; +using MiniSpace.Services.Comments.Core.Entities; +using MiniSpace.Services.Comments.Core.Repositories; +using MiniSpace.Services.Comments.Application.Commands.Handlers; +using MiniSpace.Services.Comments.Application.Commands; +using MiniSpace.Services.Comments.Infrastructure.Contexts; +using System.Threading; +using FluentAssertions; +using MiniSpace.Services.Comments.Application.Events.External.Handlers; +using MiniSpace.Services.Comments.Application.Events.External; +using System.ComponentModel.Design; + +namespace MiniSpace.Services.Comments.Application.UnitTests.Events.External.Handlers +{ + public class PostDeletedHandlerTest + { + private readonly PostDeletedHandler _postDeletedHandler; + private readonly Mock _commentRepositoryMock; + + public PostDeletedHandlerTest() + { + _commentRepositoryMock = new Mock(); + _postDeletedHandler = new PostDeletedHandler(_commentRepositoryMock.Object); + } + + [Fact] + public async Task HandleAsync_ValidData_ShouldNotThrowExeption() + { + // Arrange + var postId = System.Guid.NewGuid(); + var @event = new PostDeleted(postId); + var commentId1 = System.Guid.NewGuid(); + var commentId2 = System.Guid.NewGuid(); + var comment1 = Comment.Create(new AggregateId(commentId1), postId, + CommentContext.Event, System.Guid.NewGuid(), "Adam", System.Guid.NewGuid(), "text", DateTime.Now); + var comment2 = Comment.Create(new AggregateId(commentId2), postId, + CommentContext.Event, Guid.NewGuid(), "Bartek", Guid.NewGuid(), "text", DateTime.Now); + var commentsList = new List { comment1, comment2 }; + + _commentRepositoryMock.Setup(repo => repo.GetByPostIdAsync(postId)) + .ReturnsAsync(commentsList); + + var cancelationTocken = new CancellationToken(); + + // Act + await _postDeletedHandler.HandleAsync(@event, cancelationTocken); + + // Assert + _commentRepositoryMock.Verify(repo => repo.DeleteAsync(commentId1), Times.Once()); + _commentRepositoryMock.Verify(repo => repo.DeleteAsync(commentId2), Times.Once()); + } + } +} diff --git a/MiniSpace.Services.Comments/tests/MiniSpace.Services.Comments.Application.UnitTests/Events/External/Handlers/StudentCreatedHandlerTest.cs b/MiniSpace.Services.Comments/tests/MiniSpace.Services.Comments.Application.UnitTests/Events/External/Handlers/StudentCreatedHandlerTest.cs new file mode 100644 index 00000000..07811931 --- /dev/null +++ b/MiniSpace.Services.Comments/tests/MiniSpace.Services.Comments.Application.UnitTests/Events/External/Handlers/StudentCreatedHandlerTest.cs @@ -0,0 +1,63 @@ +using Xunit; +using Moq; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Comments.Application.Exceptions; +using MiniSpace.Services.Comments.Application.Services; +using MiniSpace.Services.Comments.Core.Entities; +using MiniSpace.Services.Comments.Core.Repositories; +using MiniSpace.Services.Comments.Application.Commands.Handlers; +using MiniSpace.Services.Comments.Application.Commands; +using MiniSpace.Services.Comments.Infrastructure.Contexts; +using System.Threading; +using FluentAssertions; +using MiniSpace.Services.Comments.Application.Events.External.Handlers; +using MiniSpace.Services.Comments.Application.Events.External; +using System.ComponentModel.Design; + +namespace MiniSpace.Services.Comments.Application.UnitTests.Events.External.Handlers +{ + public class StudentCreatedHandlerTest + { + private readonly StudentCreatedHandler _studentCreatedHandler; + private readonly Mock _studentRepositoryMock; + + public StudentCreatedHandlerTest() + { + _studentRepositoryMock = new Mock(); + _studentCreatedHandler = new StudentCreatedHandler(_studentRepositoryMock.Object); + } + + [Fact] + public async Task HandleAsync_ValidData_ShouldNotThrowExeption() + { + // Arrange + var studentId = Guid.NewGuid(); + var @event = new StudentCreated(studentId, "Jan Kowalski"); + + _studentRepositoryMock.Setup(repo => repo.ExistsAsync(studentId)) + .ReturnsAsync(false); + + // Act & Assert + Func act = async () => await _studentCreatedHandler.HandleAsync(@event); + await act.Should().NotThrowAsync(); + + } + + [Fact] + public async Task HandleAsync_StudentAlreadyCreated_ShuldThrowStudentNotFoundException() + { + // Arrange + var studentId = Guid.NewGuid(); + var @event = new StudentCreated(studentId, "Jan Kowalski"); + + _studentRepositoryMock.Setup(repo => repo.ExistsAsync(studentId)) + .ReturnsAsync(true); + + // Act & Assert + Func act = async () => await _studentCreatedHandler.HandleAsync(@event); + await act.Should().ThrowAsync(); + } + } +} diff --git a/MiniSpace.Services.Comments/tests/MiniSpace.Services.Comments.Application.UnitTests/Events/External/Handlers/StudentDeletedHandlerTest.cs b/MiniSpace.Services.Comments/tests/MiniSpace.Services.Comments.Application.UnitTests/Events/External/Handlers/StudentDeletedHandlerTest.cs new file mode 100644 index 00000000..d54b22ee --- /dev/null +++ b/MiniSpace.Services.Comments/tests/MiniSpace.Services.Comments.Application.UnitTests/Events/External/Handlers/StudentDeletedHandlerTest.cs @@ -0,0 +1,64 @@ +using Xunit; +using Moq; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Comments.Application.Exceptions; +using MiniSpace.Services.Comments.Application.Services; +using MiniSpace.Services.Comments.Core.Entities; +using MiniSpace.Services.Comments.Core.Repositories; +using MiniSpace.Services.Comments.Application.Commands.Handlers; +using MiniSpace.Services.Comments.Application.Commands; +using MiniSpace.Services.Comments.Infrastructure.Contexts; +using System.Threading; +using FluentAssertions; +using MiniSpace.Services.Comments.Application.Events.External.Handlers; +using MiniSpace.Services.Comments.Application.Events.External; +using System.ComponentModel.Design; + +namespace MiniSpace.Services.Comments.Application.UnitTests.Events.External.Handlers +{ + public class StudentDeletedHandlerTest + { + private readonly StudentDeletedHandler _studentDeletedHandler; + private readonly Mock _studentRepositoryMock; + + public StudentDeletedHandlerTest() + { + _studentRepositoryMock = new Mock(); + _studentDeletedHandler = new StudentDeletedHandler(_studentRepositoryMock.Object); + } + + [Fact] + public async Task HandleAsync_ValidData_ShouldNotThrowExeption() + { + // Arrange + var studentId = Guid.NewGuid(); + var @event = new StudentDeleted(studentId, "Jan Kowalski"); + + _studentRepositoryMock.Setup(repo => repo.ExistsAsync(studentId)) + .ReturnsAsync(true); + + // Act + await _studentDeletedHandler.HandleAsync(@event); + + // Assert + _studentRepositoryMock.Verify(repo => repo.DeleteAsync(studentId), Times.Once()); + } + + [Fact] + public async Task HandleAsync_StudentAlreadyDeleted_ShuldThrowStudentNotFoundException() + { + // Arrange + var studentId = Guid.NewGuid(); + var @event = new StudentDeleted(studentId, "Jan Kowalski"); + + _studentRepositoryMock.Setup(repo => repo.ExistsAsync(studentId)) + .ReturnsAsync(false); + + // Act & Assert + Func act = async () => await _studentDeletedHandler.HandleAsync(@event); + await act.Should().ThrowAsync(); + } + } +} diff --git a/MiniSpace.Services.Comments/tests/MiniSpace.Services.Comments.Application.UnitTests/MiniSpace.Services.Comments.Application.UnitTests.csproj b/MiniSpace.Services.Comments/tests/MiniSpace.Services.Comments.Application.UnitTests/MiniSpace.Services.Comments.Application.UnitTests.csproj new file mode 100644 index 00000000..0d888754 --- /dev/null +++ b/MiniSpace.Services.Comments/tests/MiniSpace.Services.Comments.Application.UnitTests/MiniSpace.Services.Comments.Application.UnitTests.csproj @@ -0,0 +1,32 @@ + + + + net8.0 + disable + + false + true + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + diff --git a/MiniSpace.Services.Comments/tests/MiniSpace.Services.Comments.Core.UnitTests/Entities/AggregatedIdTest.cs b/MiniSpace.Services.Comments/tests/MiniSpace.Services.Comments.Core.UnitTests/Entities/AggregatedIdTest.cs new file mode 100644 index 00000000..ff803ff3 --- /dev/null +++ b/MiniSpace.Services.Comments/tests/MiniSpace.Services.Comments.Core.UnitTests/Entities/AggregatedIdTest.cs @@ -0,0 +1,52 @@ +using Xunit; +using Moq; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Linq; +using System.Text; +using MiniSpace.Services.Comments.Application.Events; +using MiniSpace.Services.Comments.Application.Exceptions; +using MiniSpace.Services.Comments.Application.Services; +using MiniSpace.Services.Comments.Core.Entities; +using MiniSpace.Services.Comments.Core.Repositories; +using MiniSpace.Services.Comments.Application.Commands.Handlers; +using MiniSpace.Services.Comments.Application.Commands; +using MiniSpace.Services.Comments.Infrastructure.Contexts; +using Convey.CQRS.Commands; +using System.Threading; +using System.Security.Claims; +using FluentAssertions; +using MiniSpace.Services.Comments.Core.Exceptions; +using Microsoft.AspNetCore.DataProtection.KeyManagement; + +namespace MiniSpace.Services.Comments.Core.UnitTests.Entities +{ + public class AggregatedIdTest + { + [Fact] + public void AggregateId_CreatedTwice_ShuldBeDiffrent() + { + // Arrange & Act + var id1 = new AggregateId(); + var id2 = new AggregateId(); + + // Assert + Assert.NotEqual(id1.Value, id2.Value); + } + + [Fact] + public void AggregateId_CreatedTwiceSameGuid_ShuldBeSame() + { + // Arrange + var id = Guid.NewGuid(); + + // Act + var id1 = new AggregateId(id); + var id2 = new AggregateId(id); + + // Assert + Assert.Equal(id1.Value, id2.Value); + } + } +} diff --git a/MiniSpace.Services.Comments/tests/MiniSpace.Services.Comments.Core.UnitTests/Entities/CommentTest.cs b/MiniSpace.Services.Comments/tests/MiniSpace.Services.Comments.Core.UnitTests/Entities/CommentTest.cs new file mode 100644 index 00000000..37e364fa --- /dev/null +++ b/MiniSpace.Services.Comments/tests/MiniSpace.Services.Comments.Core.UnitTests/Entities/CommentTest.cs @@ -0,0 +1,120 @@ +using Xunit; +using Moq; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Linq; +using System.Text; +using MiniSpace.Services.Comments.Application.Events; +using MiniSpace.Services.Comments.Application.Exceptions; +using MiniSpace.Services.Comments.Application.Services; +using MiniSpace.Services.Comments.Core.Entities; +using MiniSpace.Services.Comments.Core.Repositories; +using MiniSpace.Services.Comments.Application.Commands.Handlers; +using MiniSpace.Services.Comments.Application.Commands; +using MiniSpace.Services.Comments.Infrastructure.Contexts; +using Convey.CQRS.Commands; +using System.Threading; +using System.Security.Claims; +using FluentAssertions; +using MiniSpace.Services.Comments.Core.Exceptions; +using Microsoft.AspNetCore.DataProtection.KeyManagement; + +namespace MiniSpace.Services.Comments.Core.UnitTests.Entities +{ + public class CommentTest + { + [Fact] + public void Like_NotLiked_ShouldAddALike() + { + // Arrange + var studentId = Guid.NewGuid(); + var likes = new HashSet() { }; + var comment = new Comment(Guid.NewGuid(), Guid.NewGuid(), CommentContext.Post, + Guid.NewGuid(), "Adam", likes, Guid.Empty, "text", + DateTime.Now, DateTime.Now, DateTime.Now, 0, false); + + // Act + comment.Like(studentId); + + // Assert + Assert.Contains(comment.Likes, id => id == studentId); + } + + [Fact] + public void Like_IsLiked_ShouldThrowStudentAlreadyLikesCommentException() + { + // Arrange + var studentId = Guid.NewGuid(); + var likes = new HashSet() { studentId }; + var comment = new Comment(Guid.NewGuid(), Guid.NewGuid(), CommentContext.Post, + Guid.NewGuid(), "Adam", likes, Guid.Empty, "text", + DateTime.Now, DateTime.Now, DateTime.Now, 0, false); + + // Act & Assert + var act = new Action(() => comment.Like(studentId)); + Assert.Throws(act); + } + + [Fact] + public void UnLike_IsLiked_ShouldRemoveALike() + { + // Arrange + var studentId = Guid.NewGuid(); + var likes = new HashSet() { studentId }; + var comment = new Comment(Guid.NewGuid(), Guid.NewGuid(), CommentContext.Post, + Guid.NewGuid(), "Adam", likes, Guid.Empty, "text", + DateTime.Now, DateTime.Now, DateTime.Now, 0, false); + + // Act + comment.UnLike(studentId); + + // Assert + Assert.DoesNotContain(comment.Likes, id => id == studentId); + } + + [Fact] + public void Like_NotLiked_ShouldThrowStudentNotLikeCommentException() + { + // Arrange + var studentId = Guid.NewGuid(); + var likes = new HashSet() { }; + var comment = new Comment(Guid.NewGuid(), Guid.NewGuid(), CommentContext.Post, + Guid.NewGuid(), "Adam", likes, Guid.Empty, "text", + DateTime.Now, DateTime.Now, DateTime.Now, 0, false); + + // Act & Assert + var act = new Action(() => comment.UnLike(studentId)); + Assert.Throws(act); + } + + [Fact] + public void Create_TooLongText_ShouldThrowInvalidCommentContentException() + { + // Arrange + var commentId = new AggregateId( Guid.NewGuid()); + var text = new String('x', 301); + + // Act & Assert + var act = new Action(() => Comment.Create(commentId, Guid.NewGuid(), CommentContext.Post, + Guid.NewGuid(), "Adam", Guid.Empty, text, DateTime.Now)); + Assert.Throws(act); + } + + [Fact] + public void Update_Empty_ShouldThrowInvalidCommentContentException() + { + // Arrange + var commentId = Guid.NewGuid(); + var text = String.Empty; + var comment = new Comment(Guid.NewGuid(), Guid.NewGuid(), CommentContext.Post, + Guid.NewGuid(), "Adam", new HashSet() { }, Guid.Empty, "text", + DateTime.Now, DateTime.Now, DateTime.Now, 0, false); + + // Act & Assert + var act = new Action(() => comment.Update(text, DateTime.Now)); + Assert.Throws(act); + } + + } +} diff --git a/MiniSpace.Services.Comments/tests/MiniSpace.Services.Comments.Core.UnitTests/MiniSpace.Services.Comments.Core.UnitTests.csproj b/MiniSpace.Services.Comments/tests/MiniSpace.Services.Comments.Core.UnitTests/MiniSpace.Services.Comments.Core.UnitTests.csproj new file mode 100644 index 00000000..0d888754 --- /dev/null +++ b/MiniSpace.Services.Comments/tests/MiniSpace.Services.Comments.Core.UnitTests/MiniSpace.Services.Comments.Core.UnitTests.csproj @@ -0,0 +1,32 @@ + + + + net8.0 + disable + + false + true + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + diff --git a/MiniSpace.Services.Comments/tests/MiniSpace.Services.Comments.Infrastructure.UnitTests/MiniSpace.Services.Comments.Infrastructure.UnitTests.csproj b/MiniSpace.Services.Comments/tests/MiniSpace.Services.Comments.Infrastructure.UnitTests/MiniSpace.Services.Comments.Infrastructure.UnitTests.csproj new file mode 100644 index 00000000..0d888754 --- /dev/null +++ b/MiniSpace.Services.Comments/tests/MiniSpace.Services.Comments.Infrastructure.UnitTests/MiniSpace.Services.Comments.Infrastructure.UnitTests.csproj @@ -0,0 +1,32 @@ + + + + net8.0 + disable + + false + true + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + diff --git a/MiniSpace.Services.Comments/tests/MiniSpace.Services.Comments.Infrastructure.UnitTests/Services/MessageBrokerTest.cs b/MiniSpace.Services.Comments/tests/MiniSpace.Services.Comments.Infrastructure.UnitTests/Services/MessageBrokerTest.cs new file mode 100644 index 00000000..0976929b --- /dev/null +++ b/MiniSpace.Services.Comments/tests/MiniSpace.Services.Comments.Infrastructure.UnitTests/Services/MessageBrokerTest.cs @@ -0,0 +1,142 @@ +using Xunit; +using Moq; +using Convey.CQRS.Events; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Convey.MessageBrokers; +using Convey.MessageBrokers.Outbox; +using Convey.MessageBrokers.RabbitMQ; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using OpenTracing; +using MiniSpace.Services.Comments.Infrastructure.Services; +using MiniSpace.Services.Comments.Application.Services; +using MiniSpace.Services.Comments.Application.Events; + +namespace MiniSpace.Services.Comments.Infrastructure.UnitTests.Services +{ + public class MessageBrokerTest + { + private readonly MessageBroker _messageBroker; + private readonly Mock _mockBusPublisher; + private readonly Mock _mockMessageOutbox; + private readonly Mock _mockContextAccessor; + private readonly Mock _mockHttpContextAccessor; + private readonly Mock _mockMessagePropertiesAccessor; + private readonly Mock _mockTracer; + private readonly Mock> _mockLogger; + + public MessageBrokerTest() + { + _mockBusPublisher = new Mock(); + _mockMessageOutbox = new Mock(); + _mockContextAccessor = new Mock(); + _mockHttpContextAccessor = new Mock(); + _mockMessagePropertiesAccessor = new Mock(); + _mockTracer = new Mock(); + _mockLogger = new Mock>(); + + _messageBroker = new MessageBroker(_mockBusPublisher.Object, _mockMessageOutbox.Object, _mockContextAccessor.Object, + _mockHttpContextAccessor.Object, _mockMessagePropertiesAccessor.Object, new RabbitMqOptions(), + _mockTracer.Object, _mockLogger.Object); + } + + [Fact] + public async Task PublishAsync_WithEventsAndOutboxDisabled_PublishesEvents() + { + //Arrange + var events = new List + { + new CommentCreated(Guid.NewGuid()) + }; + _mockMessageOutbox.Setup(x => x.Enabled).Returns(false); + + //Act + await _messageBroker.PublishAsync(events); + + //Assert + _mockMessageOutbox.Verify( + x => x.SendAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny>()), + Times.Never + ); + _mockBusPublisher.Verify( + x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny>()), Times.Exactly(events.Count) + ); + } + + [Fact] + public async Task PublishAsync_WithEventsAndOutboxEnabled_SendsMessagesToOutbox() + { + //Arrange + var events = new List + { + new CommentCreated(Guid.NewGuid()) + }; + _mockMessageOutbox.Setup(x => x.Enabled).Returns(true); + + //Act + await _messageBroker.PublishAsync(events); + + //Assert + _mockMessageOutbox.Verify( + x => x.SendAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny>()), + Times.Exactly(events.Count) + ); + _mockBusPublisher.Verify( + x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny>()), Times.Never + ); + } + + [Fact] + public async Task PublishAsync_WithoutEvents_Returns() + { + //Arrange + List events = null; + + //Act + await _messageBroker.PublishAsync(events); + + //Assert + _mockMessageOutbox.Verify( + x => x.SendAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny>()), + Times.Never + ); + _mockBusPublisher.Verify( + x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny>()), Times.Never + ); + } + + [Fact] + public async Task PublishAsync_WithNullEventAndOutboxDisabled_PublishesOneLessEvent() + { + //Arrange + var events = new List + { + new CommentCreated(Guid.NewGuid()), + null + }; + _mockMessageOutbox.Setup(x => x.Enabled).Returns(false); + + //Act + await _messageBroker.PublishAsync(events); + + //Assert + _mockMessageOutbox.Verify( + x => x.SendAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny>()), + Times.Never + ); + _mockBusPublisher.Verify( + x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny>()), Times.Exactly(events.Count - 1) + ); + } + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Api/MiniSpace.Services.Events.Api.sln b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Api/MiniSpace.Services.Events.Api.sln new file mode 100644 index 00000000..b38b0c8a --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Api/MiniSpace.Services.Events.Api.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniSpace.Services.Events.Api", "MiniSpace.Services.Events.Api.csproj", "{075CA7F0-8B43-4F73-8A1B-31C246206002}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {075CA7F0-8B43-4F73-8A1B-31C246206002}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {075CA7F0-8B43-4F73-8A1B-31C246206002}.Debug|Any CPU.Build.0 = Debug|Any CPU + {075CA7F0-8B43-4F73-8A1B-31C246206002}.Release|Any CPU.ActiveCfg = Release|Any CPU + {075CA7F0-8B43-4F73-8A1B-31C246206002}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {65FCD660-A2AF-4F11-AFD6-C07F118AC0D7} + EndGlobalSection +EndGlobal diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Api/Program.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Api/Program.cs index c1b8e866..f49a31a8 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Api/Program.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Api/Program.cs @@ -47,7 +47,7 @@ public static async Task Main(string[] args) .UseDispatcherEndpoints(endpoints => endpoints .Get("events/{eventId}") .Put("events/{eventId}") - .Post("events", + .Post("events", afterDispatch: (cmd, ctx) => ctx.Response.Created($"events/{cmd.EventId}")) .Delete("events/{eventId}") .Post("events/{eventId}/sign-up") @@ -55,8 +55,10 @@ public static async Task Main(string[] args) .Post("events/{eventId}/show-interest") .Delete("events/{eventId}/show-interest") .Post("events/{eventId}/rate") + .Delete("events/{eventId}/rate") .Get>>("events/student/{studentId}") .Get("events/{eventId}/participants") + .Get("events/{eventId}/rating") .Post("events/{eventId}/participants") .Delete("events/{eventId}/participants") ) diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Api/appsettings.docker.json b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Api/appsettings.docker.json index 13a4cdc0..33a100f0 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Api/appsettings.docker.json +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Api/appsettings.docker.json @@ -22,7 +22,11 @@ "httpClient": { "type": "fabio", "retries": 3, - "services": {} + "services": { + "students": "students-service", + "friends": "friends-service", + "organizations": "organizations-service" + } }, "jwt": { "certificate": { diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/CancelRateEvent.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/CancelRateEvent.cs new file mode 100644 index 00000000..8aeeaf5f --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/CancelRateEvent.cs @@ -0,0 +1,11 @@ +using System; +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Events.Application.Commands +{ + public class CancelRateEvent: ICommand + { + public Guid EventId { get; set; } + public Guid StudentId { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/AddEvent.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/CreateEvent.cs similarity index 65% rename from MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/AddEvent.cs rename to MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/CreateEvent.cs index 8573c759..13fc9bc4 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/AddEvent.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/CreateEvent.cs @@ -1,15 +1,18 @@ using System; +using System.Collections; +using System.Collections.Generic; using Convey.CQRS.Commands; using MiniSpace.Services.Events.Core.Entities; namespace MiniSpace.Services.Events.Application.Commands { - public class AddEvent : ICommand + public class CreateEvent : ICommand { public Guid EventId { get; } public string Name { get; } public Guid OrganizerId { get; } public Guid OrganizationId { get; } + public Guid RootOrganizationId { get; } public string StartDate { get; } public string EndDate { get; } public string BuildingName { get; } @@ -18,20 +21,23 @@ public class AddEvent : ICommand public string ApartmentNumber { get; } public string City { get; } public string ZipCode { get; } + public IEnumerable MediaFiles { get; } public string Description { get; } public int Capacity { get; } public decimal Fee { get; } public string Category { get; } public string PublishDate { get; } - public AddEvent(Guid eventId, string name, Guid organizerId, Guid organizationId, string startDate, - string endDate, string buildingName, string street, string buildingNumber, string apartmentNumber, - string city, string zipCode, string description, int capacity, decimal fee, string category, string publishDate) + public CreateEvent(Guid eventId, string name, Guid organizerId, Guid organizationId, Guid rootOrganizationId, + string startDate, string endDate, string buildingName, string street, string buildingNumber, + string apartmentNumber, string city, string zipCode, IEnumerable mediaFiles, string description, + int capacity, decimal fee, string category, string publishDate) { - EventId = eventId == Guid.Empty ? Guid.NewGuid() : eventId; + EventId = eventId; Name = name; OrganizerId = organizerId; OrganizationId = organizationId; + RootOrganizationId = rootOrganizationId; StartDate = startDate; EndDate = endDate; BuildingName = buildingName; @@ -40,6 +46,7 @@ public class AddEvent : ICommand ApartmentNumber = apartmentNumber; City = city; ZipCode = zipCode; + MediaFiles = mediaFiles; Description = description; Capacity = capacity; Fee = fee; diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/CancelRateEventHandler.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/CancelRateEventHandler.cs new file mode 100644 index 00000000..a3daeacb --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/CancelRateEventHandler.cs @@ -0,0 +1,30 @@ +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Commands; +using MiniSpace.Services.Events.Application.Exceptions; +using MiniSpace.Services.Events.Core.Repositories; + +namespace MiniSpace.Services.Events.Application.Commands.Handlers +{ + public class CancelRateEventHandler : ICommandHandler + { + private readonly IEventRepository _eventRepository; + + public CancelRateEventHandler(IEventRepository eventRepository) + { + _eventRepository = eventRepository; + } + + public async Task HandleAsync(CancelRateEvent command, CancellationToken cancellationToken) + { + var @event = await _eventRepository.GetAsync(command.EventId); + if (@event is null) + { + throw new EventNotFoundException(command.EventId); + } + + @event.CancelRate(command.StudentId); + await _eventRepository.UpdateAsync(@event); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/AddEventHandler.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/CreateEventHandler.cs similarity index 82% rename from MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/AddEventHandler.cs rename to MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/CreateEventHandler.cs index 9c284e67..1c7e6758 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/AddEventHandler.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/CreateEventHandler.cs @@ -12,7 +12,7 @@ namespace MiniSpace.Services.Events.Application.Commands.Handlers { - public class AddEventHandler: ICommandHandler + public class CreateEventHandler: ICommandHandler { private readonly IEventRepository _eventRepository; private readonly IMessageBroker _messageBroker; @@ -21,7 +21,7 @@ public class AddEventHandler: ICommandHandler private readonly IEventValidator _eventValidator; private readonly IAppContext _appContext; - public AddEventHandler(IEventRepository eventRepository, IMessageBroker messageBroker, + public CreateEventHandler(IEventRepository eventRepository, IMessageBroker messageBroker, IOrganizationsServiceClient organizationsServiceClient, IDateTimeProvider dateTimeProvider, IEventValidator eventValidator, IAppContext appContext) { @@ -33,13 +33,18 @@ public class AddEventHandler: ICommandHandler _appContext = appContext; } - public async Task HandleAsync(AddEvent command, CancellationToken cancellationToken) + public async Task HandleAsync(CreateEvent command, CancellationToken cancellationToken) { var identity = _appContext.Identity; if (!identity.IsOrganizer) throw new AuthorizedUserIsNotAnOrganizerException(identity.Id); if(identity.Id != command.OrganizerId) throw new OrganizerCannotAddEventForAnotherOrganizerException(identity.Id, command.OrganizerId); + + if (command.EventId == Guid.Empty || await _eventRepository.ExistsAsync(command.EventId)) + { + throw new InvalidEventIdException(command.EventId); + } _eventValidator.ValidateName(command.Name); _eventValidator.ValidateDescription(command.Description); @@ -50,6 +55,7 @@ public async Task HandleAsync(AddEvent command, CancellationToken cancellationTo _eventValidator.ValidateDates(startDate, endDate, "event_start_date", "event_end_date"); var address = new Address(command.BuildingName, command.Street, command.BuildingNumber, command.ApartmentNumber, command.City, command.ZipCode); + _eventValidator.ValidateMediaFiles(command.MediaFiles.ToList()); _eventValidator.ValidateCapacity(command.Capacity); _eventValidator.ValidateFee(command.Fee); var category = _eventValidator.ParseCategory(command.Category); @@ -64,7 +70,7 @@ public async Task HandleAsync(AddEvent command, CancellationToken cancellationTo state = State.ToBePublished; } - var organization = await _organizationsServiceClient.GetAsync(command.OrganizationId); + var organization = await _organizationsServiceClient.GetAsync(command.OrganizationId, command.RootOrganizationId); if (organization == null) { throw new OrganizationNotFoundException(command.OrganizationId); @@ -75,12 +81,12 @@ public async Task HandleAsync(AddEvent command, CancellationToken cancellationTo throw new OrganizerDoesNotBelongToOrganizationException(command.OrganizerId, command.OrganizationId); } - var organizer = new Organizer(command.OrganizerId, identity.Name, identity.Email, command.OrganizerId, organization.Name); + var organizer = new Organizer(command.OrganizerId, identity.Name, identity.Email, command.OrganizationId, organization.Name); var @event = Event.Create(command.EventId, command.Name, command.Description, startDate, endDate, - address, command.Capacity, command.Fee, category, state, publishDate, organizer, now); + address, command.MediaFiles, command.Capacity, command.Fee, category, state, publishDate, organizer, now); await _eventRepository.AddAsync(@event); - await _messageBroker.PublishAsync(new EventCreated(@event.Id, @event.Organizer.Id)); + await _messageBroker.PublishAsync(new EventCreated(@event.Id, @event.Organizer.Id, @event.MediaFiles)); } } } \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/UpdateEventHandler.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/UpdateEventHandler.cs index de8ea278..8b689f91 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/UpdateEventHandler.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/UpdateEventHandler.cs @@ -1,4 +1,5 @@ -using System.Threading; +using System.Linq; +using System.Threading; using System.Threading.Tasks; using Convey.CQRS.Commands; using MiniSpace.Services.Events.Application.Events; @@ -54,6 +55,7 @@ public async Task HandleAsync(UpdateEvent command, CancellationToken cancellatio var address = @event.Location.Update(command.BuildingName, command.Street, command.BuildingNumber, command.ApartmentNumber, command.City, command.ZipCode); + _eventValidator.ValidateMediaFiles(command.MediaFiles.ToList()); var capacity = command.Capacity == 0 ? @event.Capacity : command.Capacity; _eventValidator.ValidateUpdatedCapacity(capacity, @event.Capacity); var fee = command.Fee == 0 ? @event.Fee : command.Fee; @@ -71,7 +73,8 @@ public async Task HandleAsync(UpdateEvent command, CancellationToken cancellatio @event.Update(name, description, startDate, endDate, address, capacity, fee, category, state, publishDate, now); await _eventRepository.UpdateAsync(@event); - await _messageBroker.PublishAsync(new EventUpdated(@event.Id, _dateTimeProvider.Now, identity.Id)); + await _messageBroker.PublishAsync(new EventUpdated(@event.Id, _dateTimeProvider.Now, + identity.Id, @event.MediaFiles)); } } } \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/SearchEvents.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/SearchEvents.cs index 96f8ea8f..6683882b 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/SearchEvents.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/SearchEvents.cs @@ -9,6 +9,8 @@ public class SearchEvents : ICommand { public string Name { get; set; } public string Organizer { get; set; } + public Guid OrganizationId { get; set; } + public Guid RootOrganizationId { get; set; } public string Category { get; set; } public string State { get; set; } public IEnumerable Friends { get; set; } diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/UpdateEvent.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/UpdateEvent.cs index 7cc856df..c58f4b24 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/UpdateEvent.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/UpdateEvent.cs @@ -1,4 +1,6 @@ using System; +using System.Collections; +using System.Collections.Generic; using Convey.CQRS.Commands; namespace MiniSpace.Services.Events.Application.Commands @@ -16,6 +18,7 @@ public class UpdateEvent : ICommand public string ApartmentNumber { get; } public string City { get; } public string ZipCode { get; } + public IEnumerable MediaFiles { get; } public string Description { get; } public int Capacity { get; } public decimal Fee { get; } @@ -24,7 +27,8 @@ public class UpdateEvent : ICommand public UpdateEvent(Guid eventId, string name, Guid organizerId, string startDate, string endDate, string buildingName, string street, string buildingNumber, string apartmentNumber, string city, - string zipCode, string description, int capacity, decimal fee, string category, string publishDate) + string zipCode, IEnumerable mediaFiles, string description, int capacity, decimal fee, + string category, string publishDate) { EventId = eventId; Name = name; @@ -37,6 +41,7 @@ public class UpdateEvent : ICommand ApartmentNumber = apartmentNumber; City = city; ZipCode = zipCode; + MediaFiles = mediaFiles; Description = description; Capacity = capacity; Fee = fee; diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/EventDto.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/EventDto.cs index aa135206..ddc0f852 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/EventDto.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/EventDto.cs @@ -15,7 +15,7 @@ public class EventDto public DateTime StartDate { get; set; } public DateTime EndDate { get; set; } public AddressDto Location { get; set; } - //public string Image { get; set; } + public IEnumerable MediaFiles { get; set; } public int InterestedStudents { get; set; } public int SignedUpStudents { get; set; } public int Capacity { get; set; } @@ -26,7 +26,7 @@ public class EventDto public DateTime UpdatedAt { get; set; } public bool IsSignedUp { get; set; } public bool IsInterested { get; set; } - public bool HasRated { get; set; } + public int? StudentRating { get; set; } public IEnumerable FriendsInterestedIn { get; set; } public IEnumerable FriendsSignedUp { get; set; } @@ -43,6 +43,7 @@ public EventDto(Event @event, Guid studentId) StartDate = @event.StartDate; EndDate = @event.EndDate; Location = new AddressDto(@event.Location); + MediaFiles = @event.MediaFiles; InterestedStudents = @event.InterestedStudents.Count(); SignedUpStudents = @event.SignedUpStudents.Count(); Capacity = @event.Capacity; @@ -52,7 +53,7 @@ public EventDto(Event @event, Guid studentId) PublishDate = @event.PublishDate; IsSignedUp = @event.SignedUpStudents.Any(x => x.StudentId == studentId); IsInterested = @event.InterestedStudents.Any(x => x.StudentId == studentId); - HasRated = @event.Ratings.Any(x => x.StudentId == studentId); + StudentRating = @event.Ratings.FirstOrDefault(x => x.StudentId == studentId)?.Value; FriendsInterestedIn = Enumerable.Empty(); FriendsSignedUp = Enumerable.Empty(); } diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/EventRatingDto.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/EventRatingDto.cs new file mode 100644 index 00000000..fa5e434f --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/EventRatingDto.cs @@ -0,0 +1,11 @@ +using System; + +namespace MiniSpace.Services.Events.Application.DTO +{ + public class EventRatingDto + { + public Guid EventId { get; set; } + public int TotalRatings { get; set; } + public double AverageRating { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventCreated.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventCreated.cs index 0df57828..1241a51b 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventCreated.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventCreated.cs @@ -1,11 +1,14 @@ using System; +using System.Collections.Generic; using Convey.CQRS.Events; +using Convey.MessageBrokers; namespace MiniSpace.Services.Events.Application.Events { - public class EventCreated(Guid eventId, Guid organizerId) : IEvent + public class EventCreated(Guid eventId, Guid organizerId, IEnumerable mediaFilesIds) : IEvent { public Guid EventId { get; set; } = eventId; public Guid OrganizerId { get; set; } = organizerId; + public IEnumerable MediaFilesIds { get; set; } = mediaFilesIds; } } \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventDeleted.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventDeleted.cs index a39a19cb..9c74a3d3 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventDeleted.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventDeleted.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Convey.CQRS.Events; namespace MiniSpace.Services.Events.Application.Events diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventParticipantAdded.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventParticipantAdded.cs index 4dba38c1..ceca1abd 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventParticipantAdded.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventParticipantAdded.cs @@ -1,5 +1,6 @@ using System; using Convey.CQRS.Events; +using Convey.MessageBrokers; namespace MiniSpace.Services.Events.Application.Events { diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventParticipantRemoved.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventParticipantRemoved.cs index 7a4a8d09..a45a5daf 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventParticipantRemoved.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventParticipantRemoved.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Convey.CQRS.Events; +using Convey.MessageBrokers; namespace MiniSpace.Services.Events.Application.Events { diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventUpdated.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventUpdated.cs index c0e79da4..1be9a70c 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventUpdated.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventUpdated.cs @@ -1,12 +1,16 @@ using System; +using System.Collections; +using System.Collections.Generic; using Convey.CQRS.Events; +using Convey.MessageBrokers; namespace MiniSpace.Services.Events.Application.Events { - public class EventUpdated(Guid eventId, DateTime updatedAt, Guid updatedBy) : IEvent + public class EventUpdated(Guid eventId, DateTime updatedAt, Guid updatedBy, IEnumerable mediaFilesIds) : IEvent { public Guid EventId { get; set; } = eventId; public DateTime UpdatedAt { get; set; } = updatedAt; public Guid UpdatedBy { get; set; } = updatedBy; + public IEnumerable MediaFilesIds { get; set; } = mediaFilesIds; } } \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/External/Handlers/MediaFileDeletedHandler.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/External/Handlers/MediaFileDeletedHandler.cs new file mode 100644 index 00000000..74369f69 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/External/Handlers/MediaFileDeletedHandler.cs @@ -0,0 +1,32 @@ +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Events; +using MiniSpace.Services.Events.Core.Repositories; + +namespace MiniSpace.Services.Events.Application.Events.External.Handlers +{ + public class MediaFileDeletedHandler: IEventHandler + { + private readonly IEventRepository _eventRepository; + + public MediaFileDeletedHandler(IEventRepository eventRepository) + { + _eventRepository = eventRepository; + } + + public async Task HandleAsync(MediaFileDeleted @event, CancellationToken cancellationToken) + { + if(@event.Source.ToLowerInvariant() != "event") + { + return; + } + + var foundEvent = await _eventRepository.GetAsync(@event.SourceId); + if(foundEvent != null) + { + foundEvent.RemoveMediaFile(@event.MediaFileId); + await _eventRepository.UpdateAsync(foundEvent); + } + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/External/MediaFileDeleted.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/External/MediaFileDeleted.cs new file mode 100644 index 00000000..b3b300b9 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/External/MediaFileDeleted.cs @@ -0,0 +1,21 @@ +using System; +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.Events.Application.Events.External +{ + [Message("mediafiles")] + public class MediaFileDeleted: IEvent + { + public Guid MediaFileId { get; } + public Guid SourceId { get; } + public string Source { get; } + + public MediaFileDeleted(Guid mediaFileId, Guid sourceId, string source) + { + MediaFileId = mediaFileId; + SourceId = sourceId; + Source = source; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/Rejected/AddEventRejected.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/Rejected/CreateEventRejected.cs similarity index 78% rename from MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/Rejected/AddEventRejected.cs rename to MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/Rejected/CreateEventRejected.cs index 54e97286..50daa427 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/Rejected/AddEventRejected.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/Rejected/CreateEventRejected.cs @@ -5,7 +5,7 @@ namespace MiniSpace.Services.Events.Application.Events.Rejected { - public class AddEventRejected(Guid organizerId, string reason, string code) : IRejectedEvent + public class CreateEventRejected(Guid organizerId, string reason, string code) : IRejectedEvent { public Guid OrganizerId { get; } = organizerId; public string Reason { get; } = reason; diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/StudentCancelledInterestInEvent.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/StudentCancelledInterestInEvent.cs index a993018b..6feb1f56 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/StudentCancelledInterestInEvent.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/StudentCancelledInterestInEvent.cs @@ -1,5 +1,6 @@ using System; using Convey.CQRS.Events; +using Convey.MessageBrokers; namespace MiniSpace.Services.Events.Application.Events { diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/StudentCancelledSignUpToEvent.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/StudentCancelledSignUpToEvent.cs index fdfd4143..8dd4e99a 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/StudentCancelledSignUpToEvent.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/StudentCancelledSignUpToEvent.cs @@ -1,5 +1,6 @@ using System; using Convey.CQRS.Events; +using Convey.MessageBrokers; namespace MiniSpace.Services.Events.Application.Events { diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/StudentShowedInterestInEvent.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/StudentShowedInterestInEvent.cs index a6e51675..712e07a3 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/StudentShowedInterestInEvent.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/StudentShowedInterestInEvent.cs @@ -1,18 +1,19 @@ using System; using Convey.CQRS.Events; +using Convey.MessageBrokers; namespace MiniSpace.Services.Events.Application.Events { public class StudentShowedInterestInEvent: IEvent { - public Guid EventId { get; } - public Guid StudentId { get; } + public Guid EventId { get; } + public Guid StudentId { get; } - public StudentShowedInterestInEvent(Guid eventId, Guid studentId) - { - EventId = eventId; - StudentId = studentId; - } + public StudentShowedInterestInEvent(Guid eventId, Guid studentId) + { + EventId = eventId; + StudentId = studentId; + } } } \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/StudentSignedUpToEvent.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/StudentSignedUpToEvent.cs index c7ce0824..0327fe51 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/StudentSignedUpToEvent.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/StudentSignedUpToEvent.cs @@ -1,5 +1,6 @@ using System; using Convey.CQRS.Events; +using Convey.MessageBrokers; namespace MiniSpace.Services.Events.Application.Events { diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/InvalidEventIdException.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/InvalidEventIdException.cs new file mode 100644 index 00000000..89fcea7f --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/InvalidEventIdException.cs @@ -0,0 +1,15 @@ +using System; + +namespace MiniSpace.Services.Events.Application.Exceptions +{ + public class InvalidEventIdException : AppException + { + public override string Code { get; } = "invalid_event_id"; + public Guid EventId { get; } + + public InvalidEventIdException(Guid eventId) : base($"Invalid event id: {eventId}.") + { + EventId = eventId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/InvalidNumberOfEventMediaFilesException.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/InvalidNumberOfEventMediaFilesException.cs new file mode 100644 index 00000000..8c1fe162 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/InvalidNumberOfEventMediaFilesException.cs @@ -0,0 +1,15 @@ +using System; + +namespace MiniSpace.Services.Events.Application.Exceptions +{ + public class InvalidNumberOfEventMediaFilesException : AppException + { + public override string Code { get; } = "invalid_number_of_event_media_files"; + public int MediaFilesNumber { get; } + public InvalidNumberOfEventMediaFilesException(int mediaFilesNumber) + : base($"Invalid media files number: {mediaFilesNumber}. It must be less or equal 5.") + { + MediaFilesNumber = mediaFilesNumber; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/MiniSpace.Services.Events.Application.sln b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/MiniSpace.Services.Events.Application.sln new file mode 100644 index 00000000..55d0dab5 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/MiniSpace.Services.Events.Application.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniSpace.Services.Events.Application", "MiniSpace.Services.Events.Application.csproj", "{77BD0055-5CC1-48B3-AC90-39D226577330}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {77BD0055-5CC1-48B3-AC90-39D226577330}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {77BD0055-5CC1-48B3-AC90-39D226577330}.Debug|Any CPU.Build.0 = Debug|Any CPU + {77BD0055-5CC1-48B3-AC90-39D226577330}.Release|Any CPU.ActiveCfg = Release|Any CPU + {77BD0055-5CC1-48B3-AC90-39D226577330}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B7D3307B-9123-41EE-AF15-A4883F0038BB} + EndGlobalSection +EndGlobal diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetEventRating.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetEventRating.cs new file mode 100644 index 00000000..68b209d7 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetEventRating.cs @@ -0,0 +1,11 @@ +using System; +using Convey.CQRS.Queries; +using MiniSpace.Services.Events.Application.DTO; + +namespace MiniSpace.Services.Events.Application.Queries +{ + public class GetEventRating : IQuery + { + public Guid EventId { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetStudentEvents.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetStudentEvents.cs index 9a5891e0..d3ede556 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetStudentEvents.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetStudentEvents.cs @@ -9,6 +9,8 @@ namespace MiniSpace.Services.Events.Application.Queries public class GetStudentEvents : IQuery>> { public Guid StudentId { get; set; } + public string EngagementType { get; set; } + public int Page { get; set; } public int NumberOfResults { get; set; } } } \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/Clients/IOrganizationsServiceClient.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/Clients/IOrganizationsServiceClient.cs index 49d59218..ca0e95aa 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/Clients/IOrganizationsServiceClient.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/Clients/IOrganizationsServiceClient.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; using MiniSpace.Services.Events.Application.DTO; @@ -6,6 +7,7 @@ namespace MiniSpace.Services.Events.Application.Services.Clients { public interface IOrganizationsServiceClient { - Task GetAsync(Guid id); + Task GetAsync(Guid organizationId, Guid rootId); + Task> GetAllChildrenOrganizations(Guid organizationId, Guid rootId); } } \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/IEventValidator.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/IEventValidator.cs index cf1ad68b..2f15e6ec 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/IEventValidator.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/IEventValidator.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using MiniSpace.Services.Events.Core.Entities; namespace MiniSpace.Services.Events.Application.Services @@ -13,6 +14,7 @@ public interface IEventValidator (int pageNumber, int pageSize) PageFilter(int pageNumber, int pageSize); void ValidateName(string name); void ValidateDescription(string description); + void ValidateMediaFiles(List mediaFiles); void ValidateCapacity(int capacity); void ValidateFee(decimal fee); void ValidateUpdatedCapacity(int currentCapacity, int newCapacity); diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/Event.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/Event.cs index da9c0ffe..39816b87 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/Event.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/Event.cs @@ -17,7 +17,7 @@ public class Event: AggregateRoot public DateTime StartDate { get; private set; } public DateTime EndDate { get; private set; } public Address Location { get; private set; } - //public string Image { get; set; } + public IEnumerable MediaFiles { get; set; } public int Capacity { get; private set; } public decimal Fee { get; private set; } public Category Category { get; private set; } @@ -44,8 +44,8 @@ public IEnumerable Ratings } public Event(AggregateId id, string name, string description, DateTime startDate, DateTime endDate, - Address location, int capacity, decimal fee, Category category, State state, DateTime publishDate, - Organizer organizer, DateTime updatedAt, IEnumerable interestedStudents = null, + Address location, IEnumerable mediaFiles, int capacity, decimal fee, Category category, State state, + DateTime publishDate, Organizer organizer, DateTime updatedAt, IEnumerable interestedStudents = null, IEnumerable signedUpStudents = null, IEnumerable ratings = null) { Id = id; @@ -54,6 +54,7 @@ public IEnumerable Ratings StartDate = startDate; EndDate = endDate; Location = location; + MediaFiles = mediaFiles; Capacity = capacity; Fee = fee; Category = category; @@ -67,11 +68,11 @@ public IEnumerable Ratings } public static Event Create(AggregateId id, string name, string description, DateTime startDate, DateTime endDate, - Address location, int capacity, decimal fee, Category category, State state, DateTime publishDate, - Organizer organizer, DateTime now) + Address location, IEnumerable mediaFiles, int capacity, decimal fee, Category category, State state, + DateTime publishDate, Organizer organizer, DateTime now) { - var @event = new Event(id, name, description, startDate, endDate, location, capacity, fee, category, - state, publishDate, organizer, now); + var @event = new Event(id, name, description, startDate, endDate, location, mediaFiles, capacity, fee, + category, state, publishDate, organizer, now); return @event; } @@ -178,12 +179,18 @@ public void Rate(Guid studentId, int rating) throw new InvalidRatingValueException(rating); } - if (_ratings.Any(r => r.StudentId == studentId)) + _ratings.Add(new Rating(studentId, rating)); + } + + public void CancelRate(Guid studentId) + { + var rating = _ratings.SingleOrDefault(r => r.StudentId == studentId); + if (rating is null) { - throw new StudentAlreadyRatedEventException(Id, studentId); + throw new StudentNotRatedEventException(studentId, Id); } - _ratings.Add(new Rating(studentId, rating)); + _ratings.Remove(rating); } public bool UpdateState(DateTime now) @@ -213,7 +220,16 @@ private void ChangeState(State state) State = state; } + public void RemoveMediaFile(Guid mediaFileId) + { + var mediaFile = MediaFiles.SingleOrDefault(mf => mf == mediaFileId); + if (mediaFile == Guid.Empty) + { + throw new MediaFileNotFoundException(mediaFileId, Id); + } + } + public bool IsOrganizer(Guid organizerId) => Organizer.Id == organizerId; } -} \ No newline at end of file +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Exceptions/MediaFileNotFoundException.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Exceptions/MediaFileNotFoundException.cs new file mode 100644 index 00000000..305ba98d --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Exceptions/MediaFileNotFoundException.cs @@ -0,0 +1,18 @@ +using System; + +namespace MiniSpace.Services.Events.Core.Exceptions +{ + public class MediaFileNotFoundException : DomainException + { + public override string Code { get; } = "media_file_not_found"; + public Guid MediaFileId { get; } + public Guid EventId { get; } + + public MediaFileNotFoundException(Guid mediaFileId, Guid eventId) + : base($"Media file with ID: '{mediaFileId}' was not found for event with ID: {eventId}.") + { + MediaFileId = mediaFileId; + EventId = eventId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Exceptions/StudentAlreadyRatedEventException.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Exceptions/StudentAlreadyRatedEventException.cs deleted file mode 100644 index 4d782130..00000000 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Exceptions/StudentAlreadyRatedEventException.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; - -namespace MiniSpace.Services.Events.Core.Exceptions -{ - public class StudentAlreadyRatedEventException : DomainException - { - public override string Code { get; } = "student_already_rated_event"; - public Guid EventId { get; } - public Guid StudentId { get; } - - public StudentAlreadyRatedEventException(Guid eventId, Guid studentId) - : base($"Student with ID: '{studentId}' already rated event with ID: '{eventId}'.") - { - EventId = eventId; - StudentId = studentId; - } - } -} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Exceptions/StudentNotRatedEventException.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Exceptions/StudentNotRatedEventException.cs new file mode 100644 index 00000000..6023b304 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Exceptions/StudentNotRatedEventException.cs @@ -0,0 +1,18 @@ +using System; + +namespace MiniSpace.Services.Events.Core.Exceptions +{ + public class StudentNotRatedEventException : DomainException + { + public override string Code { get; } = "student_not_rated_event"; + public Guid EventId { get; } + public Guid StudentId { get; } + + public StudentNotRatedEventException(Guid eventId, Guid studentId) + : base($"Student with ID: '{studentId}' has not rated event with ID: '{eventId}'.") + { + EventId = eventId; + StudentId = studentId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/MiniSpace.Services.Events.Core.sln b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/MiniSpace.Services.Events.Core.sln new file mode 100644 index 00000000..6a3e763e --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/MiniSpace.Services.Events.Core.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniSpace.Services.Events.Core", "MiniSpace.Services.Events.Core.csproj", "{F81614CE-5BD6-4047-A4C4-6A9DADBE8A25}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F81614CE-5BD6-4047-A4C4-6A9DADBE8A25}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F81614CE-5BD6-4047-A4C4-6A9DADBE8A25}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F81614CE-5BD6-4047-A4C4-6A9DADBE8A25}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F81614CE-5BD6-4047-A4C4-6A9DADBE8A25}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {7BDB627C-3C4D-44A3-8EE6-1FA13124E296} + EndGlobalSection +EndGlobal diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Repositories/IEventRepository.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Repositories/IEventRepository.cs index bed36230..d172c448 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Repositories/IEventRepository.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Repositories/IEventRepository.cs @@ -13,12 +13,15 @@ public interface IEventRepository Task AddAsync(Event @event); Task UpdateAsync(Event @event); Task DeleteAsync(Guid id); + Task ExistsAsync(Guid id); Task<(IEnumerable events, int pageNumber,int pageSize, int totalPages, int totalElements)> BrowseEventsAsync( int pageNumber, int pageSize, string name, string organizer, DateTime dateFrom, DateTime dateTo, - Category? category, State? state, IEnumerable friends, EventEngagementType? friendsEngagementType, - IEnumerable sortBy, string direction, IEnumerable eventIds = null); + Category? category, State? state, IEnumerable organizations, IEnumerable friends, + EventEngagementType? friendsEngagementType, IEnumerable sortBy, string direction); Task<(IEnumerable events, int pageNumber,int pageSize, int totalPages, int totalElements)> BrowseOrganizerEventsAsync( int pageNumber, int pageSize, string name, Guid organizerId, DateTime dateFrom, DateTime dateTo, IEnumerable sortBy, string direction, State? state); + Task<(IEnumerable events, int pageNumber,int pageSize, int totalPages, int totalElements)> BrowseStudentEventsAsync( + int pageNumber, int pageSize, IEnumerable eventIds, IEnumerable sortBy, string direction); } } \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Exceptions/ExceptionToMessageMapper.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Exceptions/ExceptionToMessageMapper.cs index 28d680ee..75f68674 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Exceptions/ExceptionToMessageMapper.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Exceptions/ExceptionToMessageMapper.cs @@ -12,7 +12,7 @@ public object Map(Exception exception, object message) => exception switch { // TODO: Add more exceptions - AuthorizedUserIsNotAnOrganizerException ex => new AddEventRejected(ex.UserId, ex.Message, ex.Code), + AuthorizedUserIsNotAnOrganizerException ex => new CreateEventRejected(ex.UserId, ex.Message, ex.Code), EventNotFoundException ex => message switch { @@ -30,25 +30,25 @@ EventNotFoundException ex InvalidEventCategoryException ex => message switch { - AddEvent m => new AddEventRejected(m.OrganizerId, ex.Message, ex.Code), + CreateEvent m => new CreateEventRejected(m.OrganizerId, ex.Message, ex.Code), _ => null }, InvalidEventDateTimeException ex => message switch { - AddEvent m => new AddEventRejected(m.OrganizerId, ex.Message, ex.Code), + CreateEvent m => new CreateEventRejected(m.OrganizerId, ex.Message, ex.Code), _ => null }, InvalidEventDateTimeOrderException ex => message switch { - AddEvent m => new AddEventRejected(m.OrganizerId, ex.Message, ex.Code), + CreateEvent m => new CreateEventRejected(m.OrganizerId, ex.Message, ex.Code), _ => null }, OrganizerCannotAddEventForAnotherOrganizerException ex => message switch { - AddEvent m => new AddEventRejected(m.OrganizerId, ex.Message, ex.Code), + CreateEvent m => new CreateEventRejected(m.OrganizerId, ex.Message, ex.Code), _ => null }, StudentNotFoundException ex diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Extensions.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Extensions.cs index 49a53898..f38ac166 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Extensions.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Extensions.cs @@ -103,7 +103,7 @@ public static IApplicationBuilder UseInfrastructure(this IApplicationBuilder app .UseMetrics() .UseAuthentication() .UseRabbitMq() - .SubscribeCommand() + .SubscribeCommand() .SubscribeCommand() .SubscribeCommand() .SubscribeCommand() diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Logging/MessageToLogTemplateMapper.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Logging/MessageToLogTemplateMapper.cs index f9dec514..614512d2 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Logging/MessageToLogTemplateMapper.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Logging/MessageToLogTemplateMapper.cs @@ -14,10 +14,10 @@ internal sealed class MessageToLogTemplateMapper : IMessageToLogTemplateMapper => new Dictionary { { - typeof(AddEvent), + typeof(CreateEvent), new HandlerLogTemplate { - After = "Added an event with id: {EventId}." + After = "Created an event with id: {EventId}." } }, { diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/MiniSpace.Services.Events.Infrastructure.sln b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/MiniSpace.Services.Events.Infrastructure.sln new file mode 100644 index 00000000..0dfbf1c9 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/MiniSpace.Services.Events.Infrastructure.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniSpace.Services.Events.Infrastructure", "MiniSpace.Services.Events.Infrastructure.csproj", "{86A0DEA7-85B9-4D80-B43C-B41C8D985D54}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {86A0DEA7-85B9-4D80-B43C-B41C8D985D54}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {86A0DEA7-85B9-4D80-B43C-B41C8D985D54}.Debug|Any CPU.Build.0 = Debug|Any CPU + {86A0DEA7-85B9-4D80-B43C-B41C8D985D54}.Release|Any CPU.ActiveCfg = Release|Any CPU + {86A0DEA7-85B9-4D80-B43C-B41C8D985D54}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {69270B46-EAB0-4ABB-9203-6207ED340CD3} + EndGlobalSection +EndGlobal diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/EventDocument.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/EventDocument.cs index dffb5c2f..6ac0ea99 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/EventDocument.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/EventDocument.cs @@ -15,7 +15,7 @@ public class EventDocument : IIdentifiable public DateTime StartDate { get; set; } public DateTime EndDate { get; set; } public Address Location { get; set; } - //public string Image { get; set; } + public IEnumerable MediaFiles { get; set; } public IEnumerable InterestedStudents { get; set; } public IEnumerable SignedUpStudents { get; set; } public int Capacity { get; set; } diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/Extensions.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/Extensions.cs index 5464f841..12064637 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/Extensions.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/Extensions.cs @@ -18,6 +18,7 @@ public static EventDto AsDto(this EventDocument document, Guid studentId) StartDate = document.StartDate, EndDate = document.EndDate, Location = document.Location.AsDto(), + MediaFiles = document.MediaFiles, InterestedStudents = document.InterestedStudents.Count(), SignedUpStudents = document.SignedUpStudents.Count(), Capacity = document.Capacity, @@ -28,7 +29,7 @@ public static EventDto AsDto(this EventDocument document, Guid studentId) UpdatedAt = document.UpdatedAt, IsSignedUp = document.SignedUpStudents.Any(x => x.StudentId == studentId), IsInterested = document.InterestedStudents.Any(x => x.StudentId == studentId), - HasRated = document.Ratings.Any(x => x.StudentId == studentId) + StudentRating = document.Ratings.FirstOrDefault(x => x.StudentId == studentId)?.Value, }; public static EventDto AsDtoWithFriends(this EventDocument document, Guid studentId, IEnumerable friends) @@ -45,8 +46,9 @@ public static EventDto AsDtoWithFriends(this EventDocument document, Guid studen public static Event AsEntity(this EventDocument document) => new (document.Id, document.Name, document.Description, document.StartDate, document.EndDate, - document.Location, document.Capacity, document.Fee, document.Category, document.State, document.PublishDate, - document.Organizer, document.UpdatedAt,document.InterestedStudents, document.SignedUpStudents, document.Ratings); + document.Location, document.MediaFiles, document.Capacity, document.Fee, document.Category, + document.State, document.PublishDate, document.Organizer, document.UpdatedAt,document.InterestedStudents, + document.SignedUpStudents, document.Ratings); public static EventDocument AsDocument(this Event entity) => new () @@ -58,6 +60,7 @@ public static EventDocument AsDocument(this Event entity) StartDate = entity.StartDate, EndDate = entity.EndDate, Location = entity.Location, + MediaFiles = entity.MediaFiles, InterestedStudents = entity.InterestedStudents, SignedUpStudents = entity.SignedUpStudents, Capacity = entity.Capacity, @@ -76,6 +79,14 @@ public static EventParticipantsDto AsDto(this EventDocument document) InterestedStudents = document.InterestedStudents.Select(p => p.AsDto()), SignedUpStudents = document.SignedUpStudents.Select(p => p.AsDto()) }; + + public static EventRatingDto AsRatingDto(this EventDocument document) + => new () + { + EventId = document.Id, + TotalRatings = document.Ratings.Count(), + AverageRating = document.Ratings.Any() ? document.Ratings.Average(x => x.Value) : 0 + }; public static AddressDto AsDto(this Address entity) => new () diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetEventRatingHandler.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetEventRatingHandler.cs new file mode 100644 index 00000000..2e947f53 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetEventRatingHandler.cs @@ -0,0 +1,42 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Queries; +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Events.Application; +using MiniSpace.Services.Events.Application.DTO; +using MiniSpace.Services.Events.Application.Queries; +using MiniSpace.Services.Events.Infrastructure.Mongo.Documents; + +namespace MiniSpace.Services.Events.Infrastructure.Mongo.Queries.Handlers +{ + public class GetEventRatingHandler : IQueryHandler + { + private readonly IMongoRepository _eventRepository; + private readonly IAppContext _appContext; + + public GetEventRatingHandler(IMongoRepository eventRepository, + IAppContext appContext) + { + _eventRepository = eventRepository; + _appContext = appContext; + } + + public async Task HandleAsync(GetEventRating query, CancellationToken cancellationToken) + { + var document = await _eventRepository.GetAsync(p => p.Id == query.EventId); + if(document is null) + { + return null; + } + var identity = _appContext.Identity; + if(identity.IsAuthenticated && identity.Id != document.Organizer.Id && !identity.IsAdmin) + { + return null; + } + + return document.AsRatingDto(); + } + + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetStudentEventsHandler.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetStudentEventsHandler.cs index 14818f55..4950a1b0 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetStudentEventsHandler.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetStudentEventsHandler.cs @@ -21,13 +21,15 @@ public class GetStudentEventsHandler : IQueryHandler>> HandleAsync(GetStudentEv 1, query.NumberOfResults, 0, 0); } + var engagementType = _eventValidator.ParseEngagementType(query.EngagementType); + var studentEvents = await _studentsServiceClient.GetAsync(query.StudentId); - var studentEventIds = studentEvents.InterestedInEvents.Union(studentEvents.SignedUpEvents).ToList(); + var studentEventIds = engagementType switch + { + EventEngagementType.SignedUp => studentEvents.SignedUpEvents.ToList(), + EventEngagementType.InterestedIn => studentEvents.InterestedInEvents.ToList(), + _ => [] + }; - var result = await _eventRepository.BrowseEventsAsync(1, query.NumberOfResults, - string.Empty, string.Empty, DateTime.MinValue, DateTime.MinValue, null, null, - Enumerable.Empty(), null, Enumerable.Empty(), "asc", studentEventIds); + var result = await _eventRepository.BrowseStudentEventsAsync(query.Page, + query.NumberOfResults, studentEventIds, Enumerable.Empty(), "asc"); - return new PagedResponse>(result.Item1.Select(e => new EventDto(e, identity.Id)), - result.Item2, result.Item3, result.Item4, result.Item5);; + return new PagedResponse>(result.events.Select(e => new EventDto(e, identity.Id)), + result.pageNumber, result.pageSize, result.totalPages, result.totalElements); } } } \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Repositories/EventMongoRepository.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Repositories/EventMongoRepository.cs index af2c686d..a6b084d3 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Repositories/EventMongoRepository.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Repositories/EventMongoRepository.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -52,14 +53,15 @@ public async Task> GetAllAsync() public async Task<(IEnumerable events, int pageNumber,int pageSize, int totalPages, int totalElements)> BrowseEventsAsync( int pageNumber, int pageSize, string name, string organizer, DateTime dateFrom, DateTime dateTo, - Category? category, State? state, IEnumerable friends, EventEngagementType? friendsEngagementType, - IEnumerable sortBy, string direction, IEnumerable eventIds = null) + Category? category, State? state, IEnumerable organizations, IEnumerable friends, + EventEngagementType? friendsEngagementType, IEnumerable sortBy, string direction) { - var filterDefinition = Extensions.ToFilterDefinition(name, dateFrom, dateTo, eventIds) + var filterDefinition = Extensions.ToFilterDefinition(name, dateFrom, dateTo) .AddOrganizerNameFilter(organizer) .AddCategoryFilter(category) .AddRestrictedStateFilter(state) - .AddFriendsFilter(friends, friendsEngagementType); + .AddFriendsFilter(friends, friendsEngagementType) + .AddOrganizationsIdFilter(organizations); var sortDefinition = Extensions.ToSortDefinition(sortBy, direction); var pagedEvents = await BrowseAsync(filterDefinition, sortDefinition, pageNumber, pageSize); @@ -83,8 +85,23 @@ public async Task> GetAllAsync() pagedEvents.totalPages, pagedEvents.totalElements); } + public async Task<(IEnumerable events, int pageNumber, int pageSize, int totalPages, int totalElements)> BrowseStudentEventsAsync( + int pageNumber, int pageSize, IEnumerable eventIds, IEnumerable sortBy, string direction) + { + var filterDefinition = Extensions.CreateFilterDefinition() + .AddEventIdFilter(eventIds); + + var sortDefinition = Extensions.ToSortDefinition(sortBy, direction); + + var pagedEvents = await BrowseAsync(filterDefinition, sortDefinition, pageNumber, pageSize); + + return (pagedEvents.data.Select(e => e.AsEntity()), pageNumber, pageSize, + pagedEvents.totalPages, pagedEvents.totalElements); + } + public Task AddAsync(Event @event) => _repository.AddAsync(@event.AsDocument()); public Task UpdateAsync(Event @event) => _repository.UpdateAsync(@event.AsDocument()); public Task DeleteAsync(Guid id) => _repository.DeleteAsync(id); + public Task ExistsAsync(Guid id) => _repository.ExistsAsync(e => e.Id == id); } } \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Repositories/Extensions.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Repositories/Extensions.cs index 1c5d00f6..8324e288 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Repositories/Extensions.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Repositories/Extensions.cs @@ -58,11 +58,15 @@ public static class Extensions return (totalPages, (int)count, data); } + + public static FilterDefinition CreateFilterDefinition() + { + return FilterDefinitionBuilder.Empty; + } - public static FilterDefinition ToFilterDefinition(string name, DateTime dateFrom, - DateTime dateTo, IEnumerable eventIds = null) + public static FilterDefinition ToFilterDefinition(string name, DateTime dateFrom, DateTime dateTo) { - var filterDefinition = FilterDefinitionBuilder.Empty; + var filterDefinition = CreateFilterDefinition(); if (!string.IsNullOrWhiteSpace(name)) { @@ -80,11 +84,6 @@ public static class Extensions filterDefinition &= FilterDefinitionBuilder.Lte(x => x.EndDate, dateTo); } - if (eventIds != null) - { - filterDefinition &= FilterDefinitionBuilder.In(x => x.Id, eventIds); - } - return filterDefinition; } @@ -164,6 +163,25 @@ public static FilterDefinition AddRestrictedStateFilter (this Fil return filterDefinition; } + + public static FilterDefinition AddOrganizationsIdFilter(this FilterDefinition filterDefinition, + IEnumerable organizationsEnumerable) + { + var organizations = organizationsEnumerable.ToList(); + if (organizations.Count > 0) + { + filterDefinition &= FilterDefinitionBuilder.In(x => x.Organizer.OrganizationId, organizations); + } + + return filterDefinition; + } + + public static FilterDefinition AddEventIdFilter(this FilterDefinition filterDefinition, + IEnumerable eventIds) + { + filterDefinition &= FilterDefinitionBuilder.In(x => x.Id, eventIds); + return filterDefinition; + } public static SortDefinition ToSortDefinition(IEnumerable sortByArguments, string direction) { diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/Clients/OrganizationsServiceClient.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/Clients/OrganizationsServiceClient.cs index 60d490d8..e28ce968 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/Clients/OrganizationsServiceClient.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/Clients/OrganizationsServiceClient.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; using Convey.HTTP; using MiniSpace.Services.Events.Application.DTO; @@ -17,8 +18,11 @@ public OrganizationsServiceClient(IHttpClient httpClient, HttpClientOptions opti _url = options.Services["organizations"]; } - public Task GetAsync(Guid id) - => _httpClient.GetAsync($"{_url}/organizations/{id}/details"); + public Task GetAsync(Guid organizationId, Guid rootId) + => _httpClient.GetAsync($"{_url}/organizations/{organizationId}/details?rootId={rootId}"); + + public Task> GetAllChildrenOrganizations(Guid organizationId, Guid rootId) + => _httpClient.GetAsync>($"{_url}/organizations/{organizationId}/children/all?rootId={rootId}"); } } \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/EventService.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/EventService.cs index 05aff43b..8bf6ffa5 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/EventService.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/EventService.cs @@ -7,6 +7,7 @@ using MiniSpace.Services.Events.Application.DTO; using MiniSpace.Services.Events.Application.Exceptions; using MiniSpace.Services.Events.Application.Services; +using MiniSpace.Services.Events.Application.Services.Clients; using MiniSpace.Services.Events.Application.Wrappers; using MiniSpace.Services.Events.Core.Entities; using MiniSpace.Services.Events.Core.Repositories; @@ -17,12 +18,15 @@ public class EventService : IEventService { private readonly IEventRepository _eventRepository; private readonly IEventValidator _eventValidator; + private readonly IOrganizationsServiceClient _organizationsServiceClient; private readonly IAppContext _appContext; - public EventService(IEventRepository eventRepository, IEventValidator eventValidator, IAppContext appContext) + public EventService(IEventRepository eventRepository, IEventValidator eventValidator, + IOrganizationsServiceClient organizationsServiceClient, IAppContext appContext) { _eventRepository = eventRepository; _eventValidator = eventValidator; + _organizationsServiceClient = organizationsServiceClient; _appContext = appContext; } @@ -33,6 +37,7 @@ public async Task>> BrowseEventsAsync(Search Category? category = null; State? state = null; EventEngagementType? friendsEngagementType = null; + IEnumerable organizations = new List(); if(command.DateFrom != string.Empty) { dateFrom =_eventValidator.ParseDate(command.DateFrom, "DateFrom"); @@ -54,11 +59,16 @@ public async Task>> BrowseEventsAsync(Search { friendsEngagementType = _eventValidator.ParseEngagementType(command.FriendsEngagementType); } + if (command.OrganizationId != Guid.Empty && command.RootOrganizationId != Guid.Empty) + { + organizations = await _organizationsServiceClient + .GetAllChildrenOrganizations(command.OrganizationId, command.RootOrganizationId) ?? new List(); + } (int pageNumber, int pageSize) = _eventValidator.PageFilter(command.Pageable.Page, command.Pageable.Size); var result = await _eventRepository.BrowseEventsAsync( - pageNumber, pageSize, command.Name, command.Organizer, dateFrom, dateTo, category, state, command.Friends, - friendsEngagementType, command.Pageable.Sort.SortBy, command.Pageable.Sort.Direction); + pageNumber, pageSize, command.Name, command.Organizer, dateFrom, dateTo, category, state, organizations, + command.Friends, friendsEngagementType, command.Pageable.Sort.SortBy, command.Pageable.Sort.Direction); var identity = _appContext.Identity; var pagedEvents = new PagedResponse>(result.events.Select(e => new EventDto(e, identity.Id)), diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/EventValidator.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/EventValidator.cs index 780893f1..351272de 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/EventValidator.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/EventValidator.cs @@ -1,5 +1,8 @@ using System; +using System.Collections; +using System.Collections.Generic; using System.Globalization; +using System.Linq; using MiniSpace.Services.Events.Application.Exceptions; using MiniSpace.Services.Events.Application.Services; using MiniSpace.Services.Events.Core.Entities; @@ -73,6 +76,12 @@ public void ValidateDescription(string description) throw new InvalidEventDescriptionException(description); } + public void ValidateMediaFiles(List mediaFiles) + { + if (mediaFiles.Count > 5) + throw new InvalidNumberOfEventMediaFilesException(mediaFiles.Count); + } + public void ValidateCapacity(int capacity) { if (capacity <= 0 || capacity > 1000) diff --git a/MiniSpace.Services.Events/src/src.sln b/MiniSpace.Services.Events/src/src.sln new file mode 100644 index 00000000..71709094 --- /dev/null +++ b/MiniSpace.Services.Events/src/src.sln @@ -0,0 +1,43 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniSpace.Services.Events.Infrastructure", "MiniSpace.Services.Events.Infrastructure\MiniSpace.Services.Events.Infrastructure.csproj", "{AB734297-8920-4D41-90C4-9C47A4FD434F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniSpace.Services.Events.Api", "MiniSpace.Services.Events.Api\MiniSpace.Services.Events.Api.csproj", "{7C8244BA-6E05-4688-B268-7AE2DFE08AB8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniSpace.Services.Events.Core", "MiniSpace.Services.Events.Core\MiniSpace.Services.Events.Core.csproj", "{24E94BD1-4B32-4A39-9C4C-7E8BC298D51A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniSpace.Services.Events.Application", "MiniSpace.Services.Events.Application\MiniSpace.Services.Events.Application.csproj", "{3F749C01-CD5F-47AA-ABA6-85C816484803}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {AB734297-8920-4D41-90C4-9C47A4FD434F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB734297-8920-4D41-90C4-9C47A4FD434F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB734297-8920-4D41-90C4-9C47A4FD434F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB734297-8920-4D41-90C4-9C47A4FD434F}.Release|Any CPU.Build.0 = Release|Any CPU + {7C8244BA-6E05-4688-B268-7AE2DFE08AB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7C8244BA-6E05-4688-B268-7AE2DFE08AB8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7C8244BA-6E05-4688-B268-7AE2DFE08AB8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7C8244BA-6E05-4688-B268-7AE2DFE08AB8}.Release|Any CPU.Build.0 = Release|Any CPU + {24E94BD1-4B32-4A39-9C4C-7E8BC298D51A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {24E94BD1-4B32-4A39-9C4C-7E8BC298D51A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {24E94BD1-4B32-4A39-9C4C-7E8BC298D51A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {24E94BD1-4B32-4A39-9C4C-7E8BC298D51A}.Release|Any CPU.Build.0 = Release|Any CPU + {3F749C01-CD5F-47AA-ABA6-85C816484803}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3F749C01-CD5F-47AA-ABA6-85C816484803}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3F749C01-CD5F-47AA-ABA6-85C816484803}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3F749C01-CD5F-47AA-ABA6-85C816484803}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {31E8435B-1E8E-4948-903A-BBABC69BD776} + EndGlobalSection +EndGlobal diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Api/Program.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Api/Program.cs index c0456a0c..d31b1d38 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Api/Program.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Api/Program.cs @@ -39,11 +39,11 @@ public static async Task Main(string[] args) // ctx => new GetFriends { StudentId = Guid.Parse(ctx.Request.RouteValues["studentId"].ToString()) }, // (query, ctx) => ctx.Response.WriteAsJsonAsync(query), // Correctly define delegate with parameters // afterDispatch: ctx => ctx.Response.Ok()) - .Get>("friends/{studentId}") - .Get>("friends/requests/{studentId}") - .Get>("friends/pending") + .Get>("friends/{studentId}") + .Get>("friends/requests/{studentId}") + // .Get>("friends/pending") .Get>("friends/pending/all") - .Get>("friends/requests/sent/{studentId}") + .Get>("friends/requests/sent/{studentId}") // .Get("friends/requests/sent", ctx => // { // var query = new GetSentFriendRequests { StudentId = ctx.User.GetUserId() }; @@ -52,6 +52,7 @@ public static async Task Main(string[] args) .Post("friends/requests/{studentId}/accept", afterDispatch: (cmd, ctx) => ctx.Response.Ok()) .Post("friends/requests/{studentId}/decline", afterDispatch: (cmd, ctx) => ctx.Response.Ok()) + .Put("friends/requests/{studentId}/withdraw", afterDispatch: (cmd, ctx) => ctx.Response.Ok()) .Delete("friends/{requesterId}/{friendId}/remove") .Post("friends/{studentId}/invite", afterDispatch: (cmd, ctx) => ctx.Response.Created($"friends/{ctx.Request.RouteValues["studentId"]}/invite")))) .UseLogging() diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/.vscode/settings.json b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/.vscode/settings.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/.vscode/settings.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/FriendRequestSent.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/FriendRequestSent.cs deleted file mode 100644 index 90e96e2e..00000000 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/FriendRequestSent.cs +++ /dev/null @@ -1,18 +0,0 @@ - -// using Convey.CQRS.Commands; -// using Convey.CQRS.Events; - -// namespace MiniSpace.Services.Friends.Application.Events -// { -// public class FriendRequestSent : IEvent -// { -// public Guid InviterId { get; } -// public Guid InviteeId { get; } - -// public FriendRequestSent(Guid inviterId, Guid inviteeId) -// { -// InviterId = inviterId; -// InviteeId = inviteeId; -// } -// } -// } diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/AddFriendHandler.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/AddFriendHandler.cs index 470efda1..f12812c3 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/AddFriendHandler.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/AddFriendHandler.cs @@ -45,6 +45,7 @@ public async Task HandleAsync(AddFriend command, CancellationToken cancellationT } await _friendRepository.UpdateFriendshipAsync(requester); var events = _eventMapper.MapAll(requester.Events); + await _messageBroker.PublishAsync(events); } diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/FriendRequestSentHandler.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/FriendRequestSentHandler.cs deleted file mode 100644 index f48ac508..00000000 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/FriendRequestSentHandler.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Convey.CQRS.Events; -using MiniSpace.Services.Friends.Application.Events.External; -using MiniSpace.Services.Friends.Application.Exceptions; -using MiniSpace.Services.Friends.Application.Services; -using MiniSpace.Services.Friends.Core.Repositories; -using MiniSpace.Services.Friends.Core.Entities; - -namespace MiniSpace.Services.Friends.Application.Commands.Handlers -{ - public class FriendRequestSentHandler : IEventHandler - { - private readonly IFriendRepository _friendRepository; - private readonly IEventMapper _eventMapper; - private readonly IMessageBroker _messageBroker; - private readonly IAppContext _appContext; - - public FriendRequestSentHandler(IFriendRepository friendRepository, IEventMapper eventMapper, IMessageBroker messageBroker, IAppContext appContext) - { - _friendRepository = friendRepository; - _eventMapper = eventMapper; - _messageBroker = messageBroker; - _appContext = appContext; - } - - public async Task HandleAsync(FriendRequestSent @event, CancellationToken cancellationToken) - { - var now = DateTime.UtcNow; - var request = new FriendRequest( - inviterId: @event.InviterId, - inviteeId: @event.InviteeId, - requestedAt: now, - state: FriendState.Requested - ); - await _friendRepository.AddRequestAsync(request); - - var events = _eventMapper.MapAll(request.Events); - await _messageBroker.PublishAsync(events.ToArray()); - } - } -} \ No newline at end of file diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/InviteFriendHandler.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/InviteFriendHandler.cs index 492f7a48..6da33e78 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/InviteFriendHandler.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/InviteFriendHandler.cs @@ -5,19 +5,27 @@ using MiniSpace.Services.Friends.Application.Services; using MiniSpace.Services.Friends.Core.Entities; using MiniSpace.Services.Friends.Core.Repositories; +using System.Text.Json; namespace MiniSpace.Services.Friends.Application.Commands.Handlers { public class InviteFriendHandler : ICommandHandler { private readonly IFriendRequestRepository _friendRequestRepository; + private readonly IStudentRequestsRepository _studentRequestsRepository; private readonly IMessageBroker _messageBroker; private readonly IEventMapper _eventMapper; private readonly IAppContext _appContext; - public InviteFriendHandler(IFriendRequestRepository friendRequestRepository, IMessageBroker messageBroker, IEventMapper eventMapper, IAppContext appContext) + public InviteFriendHandler( + IFriendRequestRepository friendRequestRepository, + IStudentRequestsRepository studentRequestsRepository, + IMessageBroker messageBroker, + IEventMapper eventMapper, + IAppContext appContext) { _friendRequestRepository = friendRequestRepository; + _studentRequestsRepository = studentRequestsRepository; _messageBroker = messageBroker; _eventMapper = eventMapper; _appContext = appContext; @@ -46,11 +54,36 @@ public async Task HandleAsync(InviteFriend command, CancellationToken cancellati state: FriendState.Requested ); - await _friendRequestRepository.AddAsync(friendRequest); + await AddOrUpdateStudentRequest(command.InviterId, friendRequest, FriendState.Requested); + await AddOrUpdateStudentRequest(command.InviteeId, friendRequest, FriendState.Pending); - // Optionally, publish an event about the friend request + // Publish FriendInvited Event var friendInvitedEvent = new FriendInvited(command.InviterId, command.InviteeId); + string friendInvitedJson = JsonSerializer.Serialize(friendInvitedEvent); await _messageBroker.PublishAsync(friendInvitedEvent); + + // Publish FriendRequestCreated Event + var friendRequestCreatedEvent = new FriendRequestCreated(command.InviterId, command.InviteeId); + string friendRequestCreatedJson = JsonSerializer.Serialize(friendRequestCreatedEvent); + await _messageBroker.PublishAsync(friendRequestCreatedEvent); + + // Publish FriendRequestSent Event + var friendRequestSentEvent = new FriendRequestSent(command.InviterId, command.InviteeId); + string friendRequestSentJson = JsonSerializer.Serialize(friendRequestSentEvent); + await _messageBroker.PublishAsync(friendRequestSentEvent); + } + + private async Task AddOrUpdateStudentRequest(Guid studentId, FriendRequest friendRequest, FriendState state) + { + var studentRequests = await _studentRequestsRepository.GetAsync(studentId); + if (studentRequests == null) + { + studentRequests = new StudentRequests(studentId); + await _studentRequestsRepository.AddAsync(studentRequests); + } + + studentRequests.AddRequest(friendRequest.InviterId, friendRequest.InviteeId, friendRequest.RequestedAt, state); + await _studentRequestsRepository.UpdateAsync(studentRequests.StudentId, studentRequests.FriendRequests); } } diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/PendingFriendAcceptHandler.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/PendingFriendAcceptHandler.cs index be239095..b07d049f 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/PendingFriendAcceptHandler.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/PendingFriendAcceptHandler.cs @@ -4,54 +4,81 @@ using MiniSpace.Services.Friends.Application.Exceptions; using MiniSpace.Services.Friends.Application.Services; using MiniSpace.Services.Friends.Core.Entities; +using MiniSpace.Services.Friends.Application.Events.External; namespace MiniSpace.Services.Friends.Application.Commands.Handlers { public class PendingFriendAcceptHandler : ICommandHandler { - private readonly IFriendRepository _friendRepository; - private readonly IFriendRequestRepository _friendRequestRepository; + private readonly IStudentRequestsRepository _studentRequestsRepository; + private readonly IStudentFriendsRepository _studentFriendsRepository; private readonly IMessageBroker _messageBroker; private readonly IEventMapper _eventMapper; public PendingFriendAcceptHandler( - IFriendRequestRepository friendRequestRepository, - IFriendRepository friendRepository, + // IFriendRequestRepository friendRequestRepository, + IStudentFriendsRepository studentFriendsRepository, + IStudentRequestsRepository studentRequestsRepository, + // IFriendRepository friendRepository, IMessageBroker messageBroker, IEventMapper eventMapper) { - _friendRequestRepository = friendRequestRepository; - _friendRepository = friendRepository; + // _friendRequestRepository = friendRequestRepository; + _studentFriendsRepository = studentFriendsRepository; + _studentRequestsRepository = studentRequestsRepository; + // _friendRepository = friendRepository; _messageBroker = messageBroker; _eventMapper = eventMapper; } public async Task HandleAsync(PendingFriendAccept command, CancellationToken cancellationToken = default) { - // Fetch the friend request to confirm it exists and is valid - var friendRequest = await _friendRequestRepository.FindByInviterAndInvitee(command.RequesterId, command.FriendId); - if (friendRequest == null) - { - throw new FriendshipNotFoundException(command.RequesterId, command.FriendId); - } - - if (friendRequest.State != FriendState.Requested) - { - throw new InvalidOperationException("Friend request is not in the correct state to be accepted."); - } - - // Accept the friend request - friendRequest.Accept(); - await _friendRequestRepository.UpdateAsync(friendRequest); - - // Create a new friend relationship - var newFriend = new Friend(command.RequesterId, command.FriendId, DateTime.UtcNow, FriendState.Accepted); - await _friendRepository.AddAsync(newFriend); - - // Optionally, create the reciprocal friendship to reflect the two-way relationship - var reciprocalFriend = new Friend(command.FriendId, command.RequesterId, DateTime.UtcNow, FriendState.Accepted); - await _friendRepository.AddAsync(reciprocalFriend); + // Retrieve and validate the friend request between the inviter and invitee + var inviterRequests = await _studentRequestsRepository.GetAsync(command.RequesterId); + var inviteeRequests = await _studentRequestsRepository.GetAsync(command.FriendId); + var friendRequest = FindFriendRequest(inviterRequests, inviteeRequests, command.RequesterId, command.FriendId); + var friendRequestInvitee = FindFriendRequest(inviteeRequests, inviterRequests, command.RequesterId, command.FriendId); + // Update the friend request state to accepted + friendRequest.State = FriendState.Accepted; + friendRequestInvitee.State = FriendState.Accepted; + + // Save the updated FriendRequest states + await _studentRequestsRepository.UpdateAsync(command.RequesterId, inviterRequests.FriendRequests); + await _studentRequestsRepository.UpdateAsync(command.FriendId, inviteeRequests.FriendRequests); + + // Create Friend relationships in both directions + CreateAndAddFriends(command.RequesterId, command.FriendId, FriendState.Accepted); + + // Publish related events + // var events = _eventMapper.MapAll(new Core.Events.PendingFriendAccepted(command.RequesterId, command.FriendId)); + // await _messageBroker.PublishAsync(events); + + var pendingFriendAcceptedEvent = new PendingFriendAccepted(command.RequesterId, command.FriendId); + await _messageBroker.PublishAsync(pendingFriendAcceptedEvent); + } + + private FriendRequest FindFriendRequest(StudentRequests inviter, StudentRequests invitee, Guid inviterId, Guid inviteeId) + { + // Find the FriendRequest in both inviter and invitee collections + return inviter.FriendRequests.FirstOrDefault(fr => fr.InviterId == inviterId && fr.InviteeId == inviteeId) + ?? invitee.FriendRequests.FirstOrDefault(fr => fr.InviterId == inviterId && fr.InviteeId == inviteeId) + ?? throw new FriendRequestNotFoundException(inviterId, inviteeId); + } + + private async void CreateAndAddFriends(Guid inviterId, Guid inviteeId, FriendState state) + { + // Retrieve or initialize the StudentFriends for both inviter and invitee + var inviterFriends = await _studentFriendsRepository.GetAsync(inviterId) ?? new StudentFriends(inviterId); + var inviteeFriends = await _studentFriendsRepository.GetAsync(inviteeId) ?? new StudentFriends(inviteeId); + + // Add new Friend instances with accepted state + inviterFriends.AddFriend(new Friend(inviterId, inviteeId, DateTime.UtcNow, state)); + inviteeFriends.AddFriend(new Friend(inviteeId, inviterId, DateTime.UtcNow, state)); + + // Update the StudentFriends repositories + await _studentFriendsRepository.AddOrUpdateAsync(inviterFriends); + await _studentFriendsRepository.AddOrUpdateAsync(inviteeFriends); } } } diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/RemoveFriendHandler.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/RemoveFriendHandler.cs index cdccb911..6978c168 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/RemoveFriendHandler.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/RemoveFriendHandler.cs @@ -9,46 +9,53 @@ namespace MiniSpace.Services.Friends.Application.Commands.Handlers { public class RemoveFriendHandler : ICommandHandler { - private readonly IFriendRepository _friendRepository; + private readonly IStudentFriendsRepository _studentFriendsRepository; + private readonly IStudentRequestsRepository _studentRequestsRepository; private readonly IMessageBroker _messageBroker; private readonly IEventMapper _eventMapper; private readonly IAppContext _appContext; - public RemoveFriendHandler(IFriendRepository friendRepository, IMessageBroker messageBroker, IEventMapper eventMapper, IAppContext appContext) + public RemoveFriendHandler( + IStudentFriendsRepository studentFriendsRepository, + IStudentRequestsRepository studentRequestsRepository, + IMessageBroker messageBroker, + IEventMapper eventMapper, + IAppContext appContext) { - _friendRepository = friendRepository; + _studentFriendsRepository = studentFriendsRepository; + _studentRequestsRepository = studentRequestsRepository; _messageBroker = messageBroker; _eventMapper = eventMapper; _appContext = appContext; } - public async Task HandleAsync(RemoveFriend command, CancellationToken cancellationToken = default) + public async Task HandleAsync(RemoveFriend command, CancellationToken cancellationToken = default) { var identity = _appContext.Identity; - // if (!identity.IsAuthenticated) - // { - // throw new UnauthorizedFriendActionException(command.RequesterId, identity.Id); - // } - Console.WriteLine($"Handling RemoveFriend for RequesterId: {command.RequesterId} and FriendId: {command.FriendId}. Authenticated: {identity.IsAuthenticated}"); - - - var exists = await _friendRepository.IsFriendAsync(command.RequesterId, command.FriendId); - if (!exists) + Console.WriteLine($"Handling RemoveFriend for RequesterId: {command.RequesterId} and FriendId: {command.FriendId}. Authenticated: {identity.IsAuthenticated}"); + + var requesterFriends = await _studentFriendsRepository.GetAsync(command.RequesterId); + var friendFriends = await _studentFriendsRepository.GetAsync(command.FriendId); + + if (requesterFriends == null || friendFriends == null) { throw new FriendshipNotFoundException(command.RequesterId, command.FriendId); } - // Remove the friendship in both directions - await _friendRepository.RemoveFriendAsync(command.RequesterId, command.FriendId); - await _friendRepository.RemoveFriendAsync(command.FriendId, command.RequesterId); + // Call specific methods to remove the friend connection + await _studentFriendsRepository.RemoveFriendAsync(command.RequesterId, command.FriendId); + await _studentFriendsRepository.RemoveFriendAsync(command.FriendId, command.RequesterId); + + // Remove the corresponding friend requests + await _studentRequestsRepository.RemoveFriendRequestAsync(command.RequesterId, command.FriendId); + await _studentRequestsRepository.RemoveFriendRequestAsync(command.FriendId, command.RequesterId); - // Publish an event indicating the friend has been removed + // Publish events indicating the removal of pending friend requests var eventToPublish = new PendingFriendDeclined(command.RequesterId, command.FriendId); await _messageBroker.PublishAsync(eventToPublish); - - // Publish a reciprocal event for the inverse relationship var reciprocalEventToPublish = new PendingFriendDeclined(command.FriendId, command.RequesterId); await _messageBroker.PublishAsync(reciprocalEventToPublish); } + } } diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/SentFriendRequestWithdrawHandler.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/SentFriendRequestWithdrawHandler.cs new file mode 100644 index 00000000..3399942f --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/SentFriendRequestWithdrawHandler.cs @@ -0,0 +1,93 @@ +using Convey.CQRS.Commands; +using Microsoft.Extensions.Logging; +using MiniSpace.Services.Friends.Application.Events; +using MiniSpace.Services.Friends.Application.Exceptions; +using MiniSpace.Services.Friends.Application.Services; +using MiniSpace.Services.Friends.Core.Entities; +using MiniSpace.Services.Friends.Core.Repositories; +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Friends.Application.Commands.Handlers +{ + public class SentFriendRequestWithdrawHandler : ICommandHandler + { + private readonly IFriendRequestRepository _friendRequestRepository; + private readonly IStudentRequestsRepository _studentRequestsRepository; + private readonly IMessageBroker _messageBroker; + private readonly IAppContext _appContext; + private readonly ILogger _logger; + + public SentFriendRequestWithdrawHandler( + IFriendRequestRepository friendRequestRepository, + IStudentRequestsRepository studentRequestsRepository, + IMessageBroker messageBroker, + IAppContext appContext, + ILogger logger) + { + _friendRequestRepository = friendRequestRepository; + _studentRequestsRepository = studentRequestsRepository; + _messageBroker = messageBroker; + _appContext = appContext; + _logger = logger; + } + + public async Task HandleAsync(SentFriendRequestWithdraw command, CancellationToken cancellationToken = default) + { + _logger.LogInformation("Handling SentFriendRequestWithdraw command: InviterId: {InviterId}, InviteeId: {InviteeId}", command.InviterId, command.InviteeId); + + // Fetching request details for both inviter and invitee + var inviterRequests = await _studentRequestsRepository.GetAsync(command.InviterId); + var inviteeRequests = await _studentRequestsRepository.GetAsync(command.InviteeId); + + // Checking existence of friend requests in both inviter's and invitee's lists + var friendRequestForInviter = inviterRequests?.FriendRequests.FirstOrDefault(fr => fr.InviterId == command.InviterId && fr.InviteeId == command.InviteeId); + var friendRequestForInvitee = inviteeRequests?.FriendRequests.FirstOrDefault(fr => fr.InviteeId == command.InviteeId && fr.InviterId == command.InviterId); + + if (friendRequestForInviter == null || friendRequestForInvitee == null) + { + _logger.LogError("Friend request not found for InviterId: {InviterId} and InviteeId: {InviteeId}", command.InviterId, command.InviteeId); + throw new FriendRequestNotFoundException(command.InviterId, command.InviteeId); + } + + // Update the state to Cancelled for both inviter and invitee + friendRequestForInviter.State = FriendState.Cancelled; + friendRequestForInvitee.State = FriendState.Cancelled; + _logger.LogInformation("Updating friend request state to Cancelled for request ID: {RequestId}", friendRequestForInviter.Id); + + // Remove the friend request from both inviter's and invitee's lists + await UpdateAndSaveRequests(inviterRequests, friendRequestForInviter); + await UpdateAndSaveRequests(inviteeRequests, friendRequestForInvitee); + + // Optionally delete the friend request if no longer needed + await _friendRequestRepository.DeleteAsync(friendRequestForInviter.Id); + + // Publish the event + await _messageBroker.PublishAsync(new FriendRequestWithdrawn(friendRequestForInviter.InviterId, friendRequestForInvitee.InviteeId)); + _logger.LogInformation("Published FriendRequestWithdrawn event for InviterId: {InviterId} and InviteeId: {InviteeId}", friendRequestForInviter.InviterId, friendRequestForInvitee.InviteeId); + } + + private async Task UpdateAndSaveRequests(StudentRequests requests, FriendRequest friendRequest) + { + if (requests == null) + { + _logger.LogWarning("Received null StudentRequests object for FriendRequest ID: {FriendRequestId}", friendRequest.Id); + return; + } + + if (!requests.FriendRequests.Any(fr => fr.Id == friendRequest.Id)) + { + _logger.LogWarning("FriendRequest ID: {FriendRequestId} not found in the requests of Student ID: {StudentId}", friendRequest.Id, requests.StudentId); + return; + } + + requests.RemoveRequest(friendRequest.Id); + + // Save the updated list back to the repository + await _studentRequestsRepository.UpdateAsync(requests.StudentId, requests.FriendRequests.ToList()); + _logger.LogInformation("Updated and saved requests successfully for StudentId: {StudentId}, Total Requests: {Count}", requests.StudentId, requests.FriendRequests.Count()); + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/SentFriendRequestWithdraw.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/SentFriendRequestWithdraw.cs new file mode 100644 index 00000000..f888f2a8 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/SentFriendRequestWithdraw.cs @@ -0,0 +1,17 @@ +using Convey.CQRS.Commands; +using System; + +namespace MiniSpace.Services.Friends.Application.Commands +{ + public class SentFriendRequestWithdraw : ICommand + { + public Guid InviterId { get; } + public Guid InviteeId { get; } + + public SentFriendRequestWithdraw(Guid inviterId, Guid inviteeId) + { + InviterId = inviterId; + InviteeId = inviteeId; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Dto/StudentDto.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Dto/StudentDto.cs new file mode 100644 index 00000000..31b91727 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Dto/StudentDto.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Friends.Application.Dto +{ + public class StudentDto + { + public Guid Id { get; set; } + public string Email { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public int NumberOfFriends { get; set; } + public string ProfileImage { get; set; } + public string Description { get; set; } + public DateTime DateOfBirth { get; set; } + public bool EmailNotifications { get; set; } + public bool IsBanned { get; set; } + public bool IsOrganizer { get; set; } + public string State { get; set; } + public DateTime CreatedAt { get; set; } + public List InterestedInEvents { get; set; } + public List SignedUpEvents { get; set; } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Dto/StudentFriendsDto.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Dto/StudentFriendsDto.cs new file mode 100644 index 00000000..f4f3b744 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Dto/StudentFriendsDto.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Friends.Application.Dto +{ + public class StudentFriendsDto + { + public Guid StudentId { get; set; } + public List Friends { get; set; } = new List(); + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Dto/StudentRequestsDto.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Dto/StudentRequestsDto.cs new file mode 100644 index 00000000..7418eb15 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Dto/StudentRequestsDto.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Friends.Application.Dto +{ + public class StudentRequestsDto + { + public Guid Id { get; set; } + public Guid StudentId { get; set; } + public List FriendRequests { get; set; } = new List(); + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/Handlers/FriendRequestSentHandler.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/Handlers/FriendRequestSentHandler.cs new file mode 100644 index 00000000..cabfd380 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/Handlers/FriendRequestSentHandler.cs @@ -0,0 +1,63 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Friends.Application.Events.External; +using MiniSpace.Services.Friends.Application.Exceptions; +using MiniSpace.Services.Friends.Application.Services; +using MiniSpace.Services.Friends.Core.Repositories; +using MiniSpace.Services.Friends.Core.Entities; + +namespace MiniSpace.Services.Friends.Application.Commands.Handlers +{ + public class FriendRequestSentHandler : IEventHandler + { + private readonly IFriendRepository _friendRepository; + private readonly IEventMapper _eventMapper; + private readonly IMessageBroker _messageBroker; + private readonly IAppContext _appContext; + + public FriendRequestSentHandler(IFriendRepository friendRepository, IEventMapper eventMapper, IMessageBroker messageBroker, IAppContext appContext) + { + _friendRepository = friendRepository; + _eventMapper = eventMapper; + _messageBroker = messageBroker; + _appContext = appContext; + } + + // public async Task HandleAsync(FriendRequestSent @event, CancellationToken cancellationToken) + // { + // var now = DateTime.UtcNow; + // var request = new FriendRequest( + // inviterId: @event.InviterId, + // inviteeId: @event.InviteeId, + // requestedAt: now, + // state: FriendState.Requested + // ); + // await _friendRepository.AddRequestAsync(request); + + // var events = _eventMapper.MapAll(request.Events); + // await _messageBroker.PublishAsync(events.ToArray()); + // // Console.WriteLine($"FriendInvited event published: InviterId={@event.InviterId}, InviteeId={@event.InviteeId}"); + // } + + public async Task HandleAsync(FriendRequestSent @event, CancellationToken cancellationToken) + { + try + { + var request = new FriendRequest( + inviterId: @event.InviterId, + inviteeId: @event.InviteeId, + requestedAt: DateTime.UtcNow, + state: FriendState.Requested + ); + + // await _friendRepository.AddRequestAsync(request); + var events = _eventMapper.MapAll(request.Events); + await _messageBroker.PublishAsync(events.ToArray()); + } + catch (Exception ex) + { + // Console.WriteLine($"An error occurred while handling FriendRequestSent: {ex.Message}"); + throw; + } + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/Handlers/PendingFriendAcceptedHandler.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/Handlers/PendingFriendAcceptedHandler.cs index e40c59e8..f54ed37e 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/Handlers/PendingFriendAcceptedHandler.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/Handlers/PendingFriendAcceptedHandler.cs @@ -22,18 +22,15 @@ public PendingFriendAcceptedHandler(IFriendRepository friendRepository, IEventMa public async Task HandleAsync(PendingFriendAccepted @event, CancellationToken cancellationToken) { - // Fetch the friendship and check existence var friendship = await _friendRepository.GetFriendshipAsync(@event.RequesterId, @event.FriendId); if (friendship == null) { throw new FriendshipNotFoundException(@event.RequesterId, @event.FriendId); } - // Confirm the friendship friendship.MarkAsConfirmed(); await _friendRepository.UpdateFriendshipAsync(friendship); - // Create reciprocal friendship to ensure mutual visibility and interaction if (await _friendRepository.GetFriendshipAsync(@event.FriendId, @event.RequesterId) == null) { var reciprocalFriendship = new Core.Entities.Friend(@event.FriendId, @event.RequesterId, DateTime.UtcNow, Core.Entities.FriendState.Accepted); diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/FriendRequestWithdrawn.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/FriendRequestWithdrawn.cs new file mode 100644 index 00000000..3a791182 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/FriendRequestWithdrawn.cs @@ -0,0 +1,17 @@ +using Convey.CQRS.Events; +using System; + +namespace MiniSpace.Services.Friends.Application.Events +{ + public class FriendRequestWithdrawn : IEvent + { + public Guid InviterId { get; } + public Guid InviteeId { get; } + + public FriendRequestWithdrawn(Guid inviterId, Guid inviteeId) + { + InviterId = inviterId; + InviteeId = inviteeId; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/SentFriendRequestWithdraw.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/SentFriendRequestWithdraw.cs new file mode 100644 index 00000000..dfadb41f --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/SentFriendRequestWithdraw.cs @@ -0,0 +1,19 @@ +using Convey.CQRS.Commands; +using Convey.CQRS.Events; +using System; + +namespace MiniSpace.Services.Friends.Application.Events +{ + public class SentFriendRequestWithdrawHandler : IEvent + { + public Guid RequesterId { get; } + public Guid FriendId { get; } + + public SentFriendRequestWithdrawHandler(Guid requesterId, Guid friendId) + { + RequesterId = requesterId; + FriendId = friendId; + } + } +} + diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Exceptions/FriendRequestNotFoundException.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Exceptions/FriendRequestNotFoundException.cs new file mode 100644 index 00000000..a3cfa3cb --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Exceptions/FriendRequestNotFoundException.cs @@ -0,0 +1,16 @@ +namespace MiniSpace.Services.Friends.Application.Exceptions +{ + public class FriendRequestNotFoundException : AppException + { + public override string Code { get; } = "friend_request_not_found"; + public Guid InviterId { get; } + public Guid InviteeId { get; } + + public FriendRequestNotFoundException(Guid inviterId, Guid inviteeId) + : base($"Friend request from inviter '{inviterId}' to invitee '{inviteeId}' was not found.") + { + InviterId = inviterId; + InviteeId = inviteeId; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetFriends.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetFriends.cs index 3827cb48..7d315d29 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetFriends.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetFriends.cs @@ -4,7 +4,7 @@ namespace MiniSpace.Services.Friends.Application.Queries { - public class GetFriends : IQuery> + public class GetFriends : IQuery> { public Guid StudentId { get; set; } } diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetIncomingFriendRequests.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetIncomingFriendRequests.cs new file mode 100644 index 00000000..e99a8a30 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetIncomingFriendRequests.cs @@ -0,0 +1,12 @@ +using Convey.CQRS.Queries; +using MiniSpace.Services.Friends.Application.Dto; +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Friends.Application.Queries +{ + public class GetIncomingFriendRequests : IQuery> + { + public Guid StudentId { get; set; } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetSentFriendRequests.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetSentFriendRequests.cs index 269671ca..11cf088d 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetSentFriendRequests.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetSentFriendRequests.cs @@ -4,7 +4,7 @@ namespace MiniSpace.Services.Friends.Application.Queries { - public class GetSentFriendRequests : IQuery> + public class GetSentFriendRequests : IQuery> { public Guid StudentId { get; set; } } diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Services/Clients/IStudentsServiceClient.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Services/Clients/IStudentsServiceClient.cs new file mode 100644 index 00000000..0232b6df --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Services/Clients/IStudentsServiceClient.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using MiniSpace.Services.Friends.Application.Dto; + +namespace MiniSpace.Services.Friends.Application.Services.Clients +{ + public interface IStudentsServiceClient + { + Task GetAsync(Guid id); + public Task> GetAllAsync(); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Services/IEventMapper.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Services/IEventMapper.cs index 2ce1e340..e4ebf0a5 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Services/IEventMapper.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Services/IEventMapper.cs @@ -1,4 +1,5 @@ using Convey.CQRS.Events; +using MiniSpace.Services.Friends.Application.Commands; using MiniSpace.Services.Friends.Core.Events; namespace MiniSpace.Services.Friends.Application.Services @@ -7,5 +8,7 @@ public interface IEventMapper { IEvent Map(IDomainEvent @event); IEnumerable MapAll(IEnumerable events); + // IEnumerable MapAll(IDomainEvent @event); + IEnumerable MapAll(PendingFriendAccepted pendingFriendAccept); } } diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/FriendRequest.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/FriendRequest.cs index 1a691f0e..c7246264 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/FriendRequest.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/FriendRequest.cs @@ -23,10 +23,10 @@ public FriendRequest(Guid inviterId, Guid inviteeId, DateTime requestedAt, Frien _state = state; } - public void Accept() { - if (State != FriendState.Requested) + Console.WriteLine($"State: {State}"); + if (State != FriendState.Requested || State != FriendState.Pending) throw new InvalidOperationException("Only requested friend requests can be accepted."); _state = FriendState.Accepted; @@ -35,12 +35,21 @@ public void Accept() public void Decline() { - if (State != FriendState.Requested) + if (State != FriendState.Requested || State != FriendState.Pending) throw new InvalidOperationException("Only requested friend requests can be declined."); _state = FriendState.Declined; AddEvent(new FriendshipDeclined(InviterId, InviteeId)); } + public void Cancel() + { + if (State != FriendState.Requested || State != FriendState.Pending) + throw new InvalidOperationException("Only requested friend requests can be cancelled."); + + _state = FriendState.Cancelled; + AddEvent(new FriendRequestCancelled(InviterId, InviteeId)); + } + } } diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/FriendState.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/FriendState.cs index 967ab25d..667f2c27 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/FriendState.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/FriendState.cs @@ -8,6 +8,7 @@ public enum FriendState Declined, Blocked, Cancelled, - Confirmed + Confirmed, + Pending } } diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/StudentFriends.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/StudentFriends.cs new file mode 100644 index 00000000..ace98386 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/StudentFriends.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using MiniSpace.Services.Friends.Core.Events; +using MiniSpace.Services.Friends.Core.Exceptions; + +namespace MiniSpace.Services.Friends.Core.Entities +{ + public class StudentFriends : AggregateRoot + { + public Guid StudentId { get; private set; } + private List _friends; + public IEnumerable Friends => _friends.AsReadOnly(); + + public StudentFriends(Guid studentId) + { + Id = studentId; + StudentId = studentId; + _friends = new List(); + } + + // public void AddFriend(Friend friend) + // { + // _friends.Add(friend); + // } + + public void AddFriend(Friend friend) + { + if (_friends.Any(f => f.FriendId == friend.FriendId)) + { + throw new InvalidOperationException("This friend is already added."); + } + _friends.Add(friend); + } + + public void RemoveFriend(Guid friendId) + { + var friend = _friends.FirstOrDefault(f => f.FriendId == friendId); + // if (friend == null) + // { + // throw new InvalidOperationException("Friend not found."); + // } + _friends.Remove(friend); + } + + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/StudentRequests.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/StudentRequests.cs new file mode 100644 index 00000000..4bacf82b --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/StudentRequests.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using MiniSpace.Services.Friends.Core.Events; + +namespace MiniSpace.Services.Friends.Core.Entities +{ + public class StudentRequests : AggregateRoot + { + public Guid StudentId { get; private set; } + private List _friendRequests; + + public IEnumerable FriendRequests => _friendRequests.AsReadOnly(); + + public StudentRequests(Guid studentId) + { + Id = Guid.NewGuid(); + StudentId = studentId; + _friendRequests = new List(); + } + + public void AddRequest(Guid inviterId, Guid inviteeId, DateTime requestedAt, FriendState state) + { + // if (state != FriendState.Requested || state != FriendState.Pending) + // throw new ArgumentException("Initial state must be 'Requested' or 'Pending' when adding a new friend request."); + + var friendRequest = new FriendRequest(inviterId, inviteeId, requestedAt, state); + _friendRequests.Add(friendRequest); + AddEvent(new FriendRequestCreated(friendRequest)); + } + + public void AcceptRequest(Guid requestId) + { + var request = _friendRequests.Find(r => r.Id == requestId); + if (request == null) + throw new KeyNotFoundException("Friend request not found."); + + request.Accept(); + } + + public void DeclineRequest(Guid requestId) + { + var request = _friendRequests.Find(r => r.Id == requestId); + if (request == null) + throw new KeyNotFoundException("Friend request not found."); + + request.Decline(); + } + + public void RemoveRequest(Guid requestId) + { + var request = _friendRequests.Find(r => r.Id == requestId); + if (request == null) + { + // // _logger.LogWarning("Attempted to remove a friend request that does not exist: {RequestId}", requestId); + // return; // Just return without throwing exception + } + + _friendRequests.Remove(request); + AddEvent(new FriendRequestRemoved(request)); + } + + public void UpdateRequestState(Guid requestId, FriendState newState) + { + var request = FriendRequests.FirstOrDefault(r => r.Id == requestId); + if (request != null) + { + request.State = newState; + } + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Events/FriendRequestCancelled.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Events/FriendRequestCancelled.cs new file mode 100644 index 00000000..ed909078 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Events/FriendRequestCancelled.cs @@ -0,0 +1,16 @@ +using MiniSpace.Services.Friends.Core.Entities; + +namespace MiniSpace.Services.Friends.Core.Events +{ + public class FriendRequestCancelled : IDomainEvent + { + public Guid InviterId { get; private set; } + public Guid InviteeId { get; private set; } + + public FriendRequestCancelled(Guid inviterId, Guid inviteeId) + { + InviterId = inviterId; + InviteeId = inviteeId; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Events/FriendRequestRemoved.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Events/FriendRequestRemoved.cs new file mode 100644 index 00000000..c27d6336 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Events/FriendRequestRemoved.cs @@ -0,0 +1,14 @@ +using MiniSpace.Services.Friends.Core.Entities; + +namespace MiniSpace.Services.Friends.Core.Events +{ + public class FriendRequestRemoved : IDomainEvent + { + public FriendRequest FriendRequest { get; private set; } + + public FriendRequestRemoved(FriendRequest friendRequest) + { + FriendRequest = friendRequest; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Events/PendingFriendAccepted.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Events/PendingFriendAccepted.cs new file mode 100644 index 00000000..672674b1 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Events/PendingFriendAccepted.cs @@ -0,0 +1,14 @@ +namespace MiniSpace.Services.Friends.Core.Events +{ + public class PendingFriendAccepted : IDomainEvent + { + public Guid RequesterId { get; } + public Guid FriendId { get; } + + public PendingFriendAccepted(Guid requesterId, Guid friendId) + { + RequesterId = requesterId; + FriendId = friendId; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Repositories/IStudentFriendsRepository.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Repositories/IStudentFriendsRepository.cs new file mode 100644 index 00000000..9989e2e4 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Repositories/IStudentFriendsRepository.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Friends.Core.Entities; + +namespace MiniSpace.Services.Friends.Core.Repositories +{ + public interface IStudentFriendsRepository + { + Task GetAsync(Guid studentId); + Task> GetAllAsync(); + Task AddAsync(StudentFriends studentFriends); + Task UpdateAsync(StudentFriends studentFriends); + Task DeleteAsync(Guid studentId); + Task ExistsAsync(Guid studentId); + Task> GetFriendsAsync(Guid studentId); + Task AddOrUpdateAsync(StudentFriends studentFriends); + Task RemoveFriendAsync(Guid studentId, Guid friendId); + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Repositories/IStudentRequestsRepository.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Repositories/IStudentRequestsRepository.cs new file mode 100644 index 00000000..5130e7d2 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Repositories/IStudentRequestsRepository.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Friends.Core.Entities; + +namespace MiniSpace.Services.Friends.Core.Repositories +{ + public interface IStudentRequestsRepository + { + Task GetAsync(Guid studentId); + Task> GetAllAsync(); + Task AddAsync(StudentRequests studentRequests); + Task UpdateAsync(StudentRequests studentRequests); + Task UpdateAsync(Guid studentId, IEnumerable updatedFriendRequests); + Task DeleteAsync(Guid studentId); + Task RemoveFriendRequestAsync(Guid requesterId, Guid friendId); + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Extensions.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Extensions.cs index f3638bc8..657ba3b5 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Extensions.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Extensions.cs @@ -40,6 +40,8 @@ using MiniSpace.Services.Friends.Infrastructure.Mongo.Repositories; using MiniSpace.Services.Friends.Infrastructure.Services; using MiniSpace.Services.Friends.Application.Events; +using MiniSpace.Services.Notifications.Infrastructure.Services.Clients; +using MiniSpace.Services.Friends.Application.Services.Clients; namespace MiniSpace.Services.Friends.Infrastructure { @@ -49,10 +51,13 @@ public static IConveyBuilder AddInfrastructure(this IConveyBuilder builder) { builder.Services.AddTransient(); builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddTransient(); builder.Services.AddTransient(); + builder.Services.AddTransient(); builder.Services.AddTransient(ctx => ctx.GetRequiredService().Create()); builder.Services.TryDecorate(typeof(ICommandHandler<>), typeof(OutboxCommandHandlerDecorator<>)); builder.Services.TryDecorate(typeof(IEventHandler<>), typeof(OutboxEventHandlerDecorator<>)); @@ -74,6 +79,8 @@ public static IConveyBuilder AddInfrastructure(this IConveyBuilder builder) .AddHandlersLogging() .AddMongoRepository("friendRequests") .AddMongoRepository("friends") + .AddMongoRepository("student-friends") + .AddMongoRepository("student-requests") .AddWebApiSwaggerDocs() .AddCertificateAuthentication() .AddSecurity(); @@ -94,12 +101,13 @@ public static IApplicationBuilder UseInfrastructure(this IApplicationBuilder app .SubscribeCommand() .SubscribeCommand() .SubscribeCommand() - .SubscribeEvent() - .SubscribeEvent() - .SubscribeEvent() - .SubscribeEvent() - .SubscribeEvent() + .SubscribeCommand() + // .SubscribeEvent() .SubscribeEvent() + // .SubscribeEvent() + // .SubscribeEvent() + // .SubscribeEvent() + // .SubscribeEvent() .SubscribeEvent(); return app; diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Logging/MessageToLogTemplateMapper.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Logging/MessageToLogTemplateMapper.cs index a9d0381d..b22cfb76 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Logging/MessageToLogTemplateMapper.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Logging/MessageToLogTemplateMapper.cs @@ -45,6 +45,18 @@ internal sealed class MessageToLogTemplateMapper : IMessageToLogTemplateMapper { After = "Friend request sent from: {InviterId} to: {InviteeId}." } + }, + { + typeof(FriendInvited), new HandlerLogTemplate + { + After = "Friend invited by: {InviterId} to {InviteeId}. Invitation created at: {CreatedAt}." + } + }, + { + typeof(FriendRequestCreated), new HandlerLogTemplate + { + After = "Friend request created between requester: {RequesterId} and friend: {FriendId}." + } } }; diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Documents/Extensions.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Documents/Extensions.cs index 6ad98a70..3595667f 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Documents/Extensions.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Documents/Extensions.cs @@ -9,8 +9,6 @@ public static class Extensions public static Friend AsEntity(this FriendDocument document) => new Friend(document.StudentId, document.FriendId, document.CreatedAt, document.State); - - public static FriendDocument AsDocument(this Friend entity) => new FriendDocument { @@ -48,7 +46,6 @@ public static FriendRequestDocument AsDocument(this FriendRequest entity) { throw new InvalidOperationException("FriendRequest.Id must be initialized."); } - Console.WriteLine($"******************************************************Friend request state {entity.State}"); return new FriendRequestDocument { Id = entity.Id, @@ -70,5 +67,61 @@ public static FriendRequestDto AsDto(this FriendRequestDocument document) State = document.State, StudentId = document.InviteeId }; + + public static StudentFriendsDocument AsDocument(this StudentFriends entity) + => new StudentFriendsDocument + { + Id = entity.Id, + StudentId = entity.StudentId, + Friends = entity.Friends.Select(friend => friend.AsDocument()).ToList() + }; + + public static StudentFriends AsEntity(this StudentFriendsDocument document) + => new StudentFriends(document.StudentId); + + // With the correct definitions of the Object-Value method in Core. + // ... + // public static StudentFriends AsEntity(this StudentFriendsDocument document) + // { + // var studentFriends = new StudentFriends(document.StudentId); + // foreach (var friendDoc in document.Friends) + // { + // studentFriends.AddFriend(friendDoc.AsEntity()); + // } + // return studentFriends; + // } + + public static StudentRequestsDocument AsDocument(this StudentRequests entity) + => new StudentRequestsDocument + { + Id = entity.Id, + StudentId = entity.StudentId, + FriendRequests = entity.FriendRequests.Select(fr => fr.AsDocument()).ToList() + }; + + public static StudentRequests AsEntity(this StudentRequestsDocument document) + { + if (document == null) + { + throw new ArgumentNullException(nameof(document), "StudentRequestsDocument cannot be null."); + } + + var studentRequests = new StudentRequests(document.StudentId); + foreach (var friendRequestDoc in document.FriendRequests) + { + studentRequests.AddRequest(friendRequestDoc.InviterId, friendRequestDoc.InviteeId, friendRequestDoc.RequestedAt, friendRequestDoc.State); + } + return studentRequests; + } + + public static StudentRequestsDto AsDto(this StudentRequestsDocument document) + => new StudentRequestsDto + { + Id = document.Id, + StudentId = document.StudentId, + FriendRequests = document.FriendRequests.Select(fr => fr.AsDto()).ToList() + }; + + } } diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Documents/StudentFriendsDocument.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Documents/StudentFriendsDocument.cs new file mode 100644 index 00000000..f09c326e --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Documents/StudentFriendsDocument.cs @@ -0,0 +1,14 @@ +using Convey.Types; +using MiniSpace.Services.Friends.Core.Entities; +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Friends.Infrastructure.Mongo.Documents +{ + public class StudentFriendsDocument : IIdentifiable + { + public Guid Id { get; set; } + public Guid StudentId { get; set; } + public List Friends { get; set; } = new List(); // List of friend documents + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Documents/StudentRequestsDocument.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Documents/StudentRequestsDocument.cs new file mode 100644 index 00000000..ee250c2d --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Documents/StudentRequestsDocument.cs @@ -0,0 +1,14 @@ +using Convey.Types; +using MiniSpace.Services.Friends.Core.Entities; +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Friends.Infrastructure.Mongo.Documents +{ + public class StudentRequestsDocument : IIdentifiable + { + public Guid Id { get; set; } + public Guid StudentId { get; set; } + public List FriendRequests { get; set; } = new List(); + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetFriendRequestsHandler.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetFriendRequestsHandler.cs index e17f2900..338f4a43 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetFriendRequestsHandler.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetFriendRequestsHandler.cs @@ -21,15 +21,15 @@ public GetFriendRequestsHandler(IMongoRepository fr public async Task> HandleAsync(GetFriendRequests query, CancellationToken cancellationToken) { string queryJson = JsonSerializer.Serialize(query); - Console.WriteLine($"Handling GetFriendRequests: {queryJson}"); - Console.WriteLine($"Handling GetFriendRequests for UserId: {query.StudentId}"); + // Console.WriteLine($"Handling GetFriendRequests: {queryJson}"); + // Console.WriteLine($"Handling GetFriendRequests for UserId: {query.StudentId}"); var documents = await _friendRequestRepository.FindAsync(p => p.InviteeId == query.StudentId && p.State == FriendState.Requested); - Console.WriteLine($"Found {documents.Count()} friend requests."); + // Console.WriteLine($"Found {documents.Count()} friend requests."); if (!documents.Any()) { - Console.WriteLine($"No friend requests found for UserId: {query.StudentId}."); + // Console.WriteLine($"No friend requests found for UserId: {query.StudentId}."); return Enumerable.Empty(); } diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetFriendsHandler.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetFriendsHandler.cs index 2004a2b2..dfb1094f 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetFriendsHandler.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetFriendsHandler.cs @@ -1,25 +1,49 @@ using Convey.CQRS.Queries; -using Convey.Persistence.MongoDB; using MiniSpace.Services.Friends.Application.Dto; using MiniSpace.Services.Friends.Application.Queries; +using MiniSpace.Services.Friends.Core.Repositories; using MiniSpace.Services.Friends.Infrastructure.Mongo.Documents; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; namespace MiniSpace.Services.Friends.Infrastructure.Mongo.Queries.Handlers { - public class GetFriendsHandler : IQueryHandler> + public class GetFriendsHandler : IQueryHandler> { - private readonly IMongoRepository _friendRepository; + private readonly IStudentFriendsRepository _studentFriendsRepository; - public GetFriendsHandler(IMongoRepository friendRepository) + public GetFriendsHandler(IStudentFriendsRepository studentFriendsRepository) { - _friendRepository = friendRepository; + _studentFriendsRepository = studentFriendsRepository; } - public async Task> HandleAsync(GetFriends query, CancellationToken cancellationToken) + public async Task> HandleAsync(GetFriends query, CancellationToken cancellationToken) { - var documents = await _friendRepository.FindAsync(p => p.StudentId == query.StudentId); + var friends = await _studentFriendsRepository.GetFriendsAsync(query.StudentId); + if (!friends.Any()) + { + return Enumerable.Empty(); + } - return documents.Select(doc => doc.AsDto()); + return new List + { + new StudentFriendsDto + { + StudentId = query.StudentId, + Friends = friends.Select(f => new FriendDto + { + Id = f.Id, + StudentId = f.StudentId, + FriendId = f.FriendId, + CreatedAt = f.CreatedAt, + State = f.FriendState + }).ToList() + } + }; } } + } diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetIncomingFriendRequestsHandler.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetIncomingFriendRequestsHandler.cs new file mode 100644 index 00000000..e58d0791 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetIncomingFriendRequestsHandler.cs @@ -0,0 +1,58 @@ +using Convey.CQRS.Queries; +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Friends.Application.Dto; +using MiniSpace.Services.Friends.Application.Queries; +using MiniSpace.Services.Friends.Infrastructure.Mongo.Documents; +using MongoDB.Driver; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Friends.Infrastructure.Mongo.Queries.Handlers +{ + public class GetIncomingFriendRequestsHandler : IQueryHandler> + { + private readonly IMongoRepository _studentRequestsRepository; + + public GetIncomingFriendRequestsHandler(IMongoRepository studentRequestsRepository) + { + _studentRequestsRepository = studentRequestsRepository; + } + + public async Task> HandleAsync(GetIncomingFriendRequests query, CancellationToken cancellationToken) + { + var studentRequests = await _studentRequestsRepository.Collection + .Find(doc => doc.StudentId == query.StudentId) + .ToListAsync(cancellationToken); + + if (studentRequests == null || !studentRequests.Any()) + { + return Enumerable.Empty(); + } + + var incomingRequests = studentRequests + .Select(doc => new StudentRequestsDto + { + Id = doc.Id, + StudentId = doc.StudentId, + FriendRequests = doc.FriendRequests + .Where(request => request.InviteeId == query.StudentId && request.State != Core.Entities.FriendState.Accepted) + .Select(request => new FriendRequestDto + { + Id = request.Id, + InviterId = request.InviterId, + InviteeId = request.InviteeId, + RequestedAt = request.RequestedAt, + State = request.State + }) + .ToList() + }) + .Where(dto => dto.FriendRequests.Any()) + .ToList(); + + return incomingRequests; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetSentFriendRequestsHandler.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetSentFriendRequestsHandler.cs index 2f1635ac..f144efc1 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetSentFriendRequestsHandler.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetSentFriendRequestsHandler.cs @@ -7,46 +7,53 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; namespace MiniSpace.Services.Friends.Infrastructure.Mongo.Queries.Handlers { - public class GetSentFriendRequestsHandler : IQueryHandler> + public class GetSentFriendRequestsHandler : IQueryHandler> { - private readonly IMongoRepository _friendRequestRepository; + private readonly IMongoRepository _studentRequestsRepository; - public GetSentFriendRequestsHandler(IMongoRepository friendRequestRepository) + public GetSentFriendRequestsHandler(IMongoRepository studentRequestsRepository) { - _friendRequestRepository = friendRequestRepository; + _studentRequestsRepository = studentRequestsRepository; } - public async Task> HandleAsync(GetSentFriendRequests query, CancellationToken cancellationToken) + public async Task> HandleAsync(GetSentFriendRequests query, CancellationToken cancellationToken) { - Console.WriteLine($"Fetching sent friend requests for student ID: {query.StudentId}"); + var studentRequests = await _studentRequestsRepository.Collection + .Find(doc => doc.StudentId == query.StudentId) + .ToListAsync(cancellationToken); - // Define options including the cancellation token - - // Assuming FindAsync only needs the filter and uses an options object for the cancellation token - var documents = await _friendRequestRepository.FindAsync( - doc => doc.InviterId == query.StudentId - - ); - - if (!documents.Any()) { - Console.WriteLine("No documents found"); - return Enumerable.Empty(); + if (studentRequests == null || !studentRequests.Any()) + { + return Enumerable.Empty(); } - return documents.Select(doc => new FriendRequestDto - { - Id = doc.Id, - InviterId = doc.InviterId, - InviteeId = doc.InviteeId, - RequestedAt = doc.RequestedAt, - State = doc.State - }); + var sentRequests = studentRequests + .Select(doc => new StudentRequestsDto + { + Id = doc.Id, + StudentId = doc.StudentId, + FriendRequests = doc.FriendRequests + .Where(request => request.InviterId == query.StudentId && request.State == Core.Entities.FriendState.Requested) + .Select(request => new FriendRequestDto + { + Id = request.Id, + InviterId = request.InviterId, + InviteeId = request.InviteeId, + RequestedAt = request.RequestedAt, + State = request.State, + StudentId = request.InviterId + }) + .ToList() + }) + .Where(dto => dto.FriendRequests.Any()) + .ToList(); + + return sentRequests; } - - } } diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Repositories/FriendRequestMongoRepository.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Repositories/FriendRequestMongoRepository.cs index 31690bfc..d910bb3a 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Repositories/FriendRequestMongoRepository.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Repositories/FriendRequestMongoRepository.cs @@ -38,11 +38,11 @@ public async Task UpdateAsync(FriendRequest friendRequest) documentToUpdate.State = friendRequest.State; - Console.WriteLine("Attempting to update document in database: " + JsonSerializer.Serialize(documentToUpdate)); + // Console.WriteLine("Attempting to update document in database: " + JsonSerializer.Serialize(documentToUpdate)); await _repository.UpdateAsync(documentToUpdate); var documentAfterUpdate = await _repository.GetAsync(friendRequest.Id); - Console.WriteLine("Document after update: " + JsonSerializer.Serialize(documentAfterUpdate)); + // Console.WriteLine("Document after update: " + JsonSerializer.Serialize(documentAfterUpdate)); } public async Task DeleteAsync(Guid id) diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Repositories/StudentFriendsMongoRepository.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Repositories/StudentFriendsMongoRepository.cs new file mode 100644 index 00000000..872613e2 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Repositories/StudentFriendsMongoRepository.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Friends.Core.Entities; +using MiniSpace.Services.Friends.Core.Repositories; +using MiniSpace.Services.Friends.Infrastructure.Mongo.Documents; +using MongoDB.Driver; + +public class StudentFriendsMongoRepository : IStudentFriendsRepository +{ + private readonly IMongoRepository _repository; + + public StudentFriendsMongoRepository(IMongoRepository repository) + { + _repository = repository; + } + + public async Task GetAsync(Guid studentId) + { + var document = await _repository.GetAsync(studentId); + return document?.AsEntity(); + } + + public async Task> GetAllAsync() + { + var documents = await _repository.FindAsync(_ => true); + return documents.Select(doc => doc.AsEntity()); + } + + public async Task AddAsync(StudentFriends studentFriends) + { + var document = studentFriends.AsDocument(); + await _repository.AddAsync(document); + } + + public async Task UpdateAsync(StudentFriends studentFriends) + { + var document = studentFriends.AsDocument(); + await _repository.UpdateAsync(document); + } + + public async Task DeleteAsync(Guid studentId) + { + await _repository.DeleteAsync(studentId); + } + + public async Task ExistsAsync(Guid studentId) + { + var document = await _repository.GetAsync(studentId); + return document != null; + } + + public async Task> GetFriendsAsync(Guid studentId) + { + // Using a LINQ expression instead of a MongoDB filter + var documents = await _repository.FindAsync(doc => doc.StudentId == studentId); + if (documents == null || !documents.Any()) + { + // Console.WriteLine($"No document found for student ID: {studentId}"); + return Enumerable.Empty(); + } + + var document = documents.First(); // Assuming you expect only one document per studentId or taking the first one + // Console.WriteLine($"Document found: {document.StudentId}, Friends Count: {document.Friends.Count}"); + return document.Friends.Select(doc => new Friend( + doc.StudentId, + doc.FriendId, + doc.CreatedAt, + doc.State)).ToList(); + } + + + + public async Task AddOrUpdateAsync(StudentFriends studentFriends) +{ + // Ensuring that the document ID (MongoDB _id) is explicitly set to StudentId + var filter = Builders.Filter.Eq(doc => doc.StudentId, studentFriends.StudentId); + var update = Builders.Update + .SetOnInsert(doc => doc.StudentId, studentFriends.StudentId) // Ensuring the document _id is set to StudentId on insert + .Set(doc => doc.Id, studentFriends.StudentId) // Setting the document _id field explicitly + .AddToSetEach(doc => doc.Friends, studentFriends.Friends.Select(f => f.AsDocument())); // Use AddToSetEach to append new items to the list + + var options = new UpdateOptions { IsUpsert = true }; + var result = await _repository.Collection.UpdateOneAsync(filter, update, options); + + // Console.WriteLine("********************************************************"); + // Check if the document was actually inserted or updated + if (result.ModifiedCount > 0 || result.UpsertedId != null) + { + // Retrieve the updated or inserted document + var updatedDocument = await _repository.GetAsync(studentFriends.StudentId); + if (updatedDocument != null) + { + // Serialize the updated document to JSON and log it + var json = JsonSerializer.Serialize(updatedDocument, new JsonSerializerOptions { WriteIndented = true }); + // Console.WriteLine("Updated StudentFriends document:"); + // Console.WriteLine(json); + } + else + { + // Console.WriteLine("Failed to retrieve the updated document."); + } + } + else + { + // Console.WriteLine("No changes were made to the document."); + } +} + +public async Task RemoveFriendAsync(Guid studentId, Guid friendId) +{ + var filter = Builders.Filter.Eq(doc => doc.StudentId, studentId); + var update = Builders.Update.PullFilter(doc => doc.Friends, Builders.Filter.Eq("FriendId", friendId)); + + var result = await _repository.Collection.UpdateOneAsync(filter, update); + + if (result.ModifiedCount == 0) + { + // Console.WriteLine($"No friend removed for Student ID: {studentId} with Friend ID: {friendId}"); + } + else + { + // Console.WriteLine($"Friend ID: {friendId} removed from Student ID: {studentId}'s friends list."); + } +} + + + + +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Repositories/StudentRequestsMongoRepository.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Repositories/StudentRequestsMongoRepository.cs new file mode 100644 index 00000000..cf2e01c0 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Repositories/StudentRequestsMongoRepository.cs @@ -0,0 +1,130 @@ +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Friends.Core.Entities; +using MiniSpace.Services.Friends.Core.Repositories; +using MiniSpace.Services.Friends.Infrastructure.Mongo.Documents; +using MongoDB.Driver; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Friends.Infrastructure.Mongo.Repositories +{ + public class StudentRequestsMongoRepository : IStudentRequestsRepository + { + private readonly IMongoRepository _repository; + + public StudentRequestsMongoRepository(IMongoRepository repository) + { + _repository = repository; + } + + public async Task GetAsync(Guid studentId) + { + // Console.WriteLine($"{studentId}"); + var document = await _repository.FindAsync(doc => doc.StudentId == studentId); + var studentRequestDocument = document.SingleOrDefault(); + if (studentRequestDocument == null) + { + return null; + } + + var entity = studentRequestDocument.AsEntity(); + var json = JsonSerializer.Serialize(entity, new JsonSerializerOptions { WriteIndented = true }); + // Console.WriteLine(json); + + return entity; + } + + + public async Task> GetAllAsync() + { + var documents = await _repository.FindAsync(_ => true); + return documents.Select(doc => doc.AsEntity()); + } + + public async Task AddAsync(StudentRequests studentRequests) + { + var document = studentRequests.AsDocument(); + await _repository.AddAsync(document); + } + + public async Task UpdateAsync(StudentRequests studentRequests) + { + var document = studentRequests.AsDocument(); + await _repository.UpdateAsync(document); + } + + public async Task UpdateAsync(Guid studentId, IEnumerable updatedFriendRequests) + { + var document = await _repository.FindAsync(doc => doc.StudentId == studentId); + var studentRequestDocument = document.SingleOrDefault(); + // Console.WriteLine($"*******************************************************************************"); + if (studentRequestDocument == null) + { + // Console.WriteLine($"No document found with Student ID: {studentId}"); + return; // Consider handling this case appropriately, possibly by adding a new document. + } + + // Console.WriteLine($"Before update - Document JSON: {JsonSerializer.Serialize(studentRequestDocument, new JsonSerializerOptions { WriteIndented = true })}"); + + // Convert each FriendRequest to a FriendRequestDocument before assignment + studentRequestDocument.FriendRequests = updatedFriendRequests.Select(fr => fr.AsDocument()).ToList(); + + var filter = Builders.Filter.Eq(doc => doc.StudentId, studentRequestDocument.StudentId); + var update = Builders.Update.Set(doc => doc.FriendRequests, studentRequestDocument.FriendRequests); + + var result = await _repository.Collection.UpdateOneAsync(filter, update); + + // Fetch the updated document to log its new state + var updatedDocument = await _repository.FindAsync(doc => doc.StudentId == studentId); + var updatedStudentRequestDocument = updatedDocument.SingleOrDefault(); + + // Console.WriteLine($"After update - Document JSON: {JsonSerializer.Serialize(updatedStudentRequestDocument, new JsonSerializerOptions { WriteIndented = true })}"); + + if (result.ModifiedCount == 0) + { + // Console.WriteLine("No documents were modified during the update operation."); + throw new Exception("Update failed, no document was modified."); + } + else + { + // Console.WriteLine($"Document with Student ID: {studentId} was successfully updated. Modified count: {result.ModifiedCount}"); + } + } + + public async Task DeleteAsync(Guid studentId) + { + var documents = await _repository.FindAsync(doc => doc.StudentId == studentId); + var document = documents.SingleOrDefault(); + if (document != null) + { + await _repository.DeleteAsync(document.Id); + } + } + + public async Task RemoveFriendRequestAsync(Guid requesterId, Guid friendId) + { + var filter = Builders.Filter.Eq(doc => doc.StudentId, requesterId) & + Builders.Filter.Or( + Builders.Filter.ElemMatch(doc => doc.FriendRequests, Builders.Filter.Eq(fr => fr.InviterId, friendId)), + Builders.Filter.ElemMatch(doc => doc.FriendRequests, Builders.Filter.Eq(fr => fr.InviteeId, friendId)) + ); + + var update = Builders.Update.PullFilter(doc => doc.FriendRequests, + Builders.Filter.Or( + Builders.Filter.Eq(fr => fr.InviterId, friendId), + Builders.Filter.Eq(fr => fr.InviteeId, friendId) + )); + + var result = await _repository.Collection.UpdateOneAsync(filter, update); + + if (result.ModifiedCount == 0) + { + throw new Exception("No friend request was removed."); + } + } + + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Services/Clients/StudentsServiceClient.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Services/Clients/StudentsServiceClient.cs new file mode 100644 index 00000000..5e71d3cf --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Services/Clients/StudentsServiceClient.cs @@ -0,0 +1,27 @@ +using System; +using System.Threading.Tasks; +using Convey.HTTP; +using MiniSpace.Services.Friends.Application.Dto; +using MiniSpace.Services.Friends.Application.Services.Clients; + +namespace MiniSpace.Services.Notifications.Infrastructure.Services.Clients +{ + public class StudentsServiceClient : IStudentsServiceClient + { + private readonly IHttpClient _httpClient; + private readonly string _url; + + public StudentsServiceClient(IHttpClient httpClient, HttpClientOptions options) + { + _httpClient = httpClient; + _url = options.Services["students"]; + } + + public Task GetAsync(Guid id) + => _httpClient.GetAsync($"{_url}/students/{id}"); + + public Task> GetAllAsync() + => _httpClient.GetAsync>($"{_url}/students"); + + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Services/EventMapper.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Services/EventMapper.cs index 509da8fe..6a533614 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Services/EventMapper.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Services/EventMapper.cs @@ -2,6 +2,7 @@ using MiniSpace.Services.Friends.Application.Events; using MiniSpace.Services.Friends.Application.Events.External; using MiniSpace.Services.Friends.Application.Services; +using MiniSpace.Services.Friends.Core.Entities; using MiniSpace.Services.Friends.Core.Events; namespace MiniSpace.Services.Friends.Infrastructure.Services @@ -21,15 +22,30 @@ public IEvent Map(IDomainEvent @event) case Core.Events.FriendRemoved e: return new Application.Events.FriendRemoved(e.Requester.Id, e.Friend.Id); - case Core.Events.FriendshipConfirmed e: - return new PendingFriendAccepted(e.FriendId, e.FriendId); + // case Core.Events.FriendshipConfirmed e: + // return new PendingFriendAccepted(e.FriendId, e.FriendId); case Core.Events.FriendshipDeclined e: return new PendingFriendDeclined(e.RequesterId, e.FriendId); + + case Core.Events.FriendInvited e: + return new Application.Events.External.FriendInvited(e.Inviter.Id, e.Invitee.Id); + + case FriendRequest e when !(e is Core.Events.FriendInvited): + return new Application.Events.External.FriendRequestCreated(e.InviterId, e.InviteeId); + + case Core.Events.PendingFriendAccepted e: + return new Application.Events.External.PendingFriendAccepted(e.RequesterId, e.FriendId); default: return null; } } + + public IEnumerable MapAll(Core.Events.PendingFriendAccepted pendingFriendAccept) + { + // Implementation for the new method + return new List { new Application.Events.External.PendingFriendAccepted(pendingFriendAccept.RequesterId, pendingFriendAccept.FriendId) }; + } } } diff --git a/MiniSpace.Services.MediaFiles/.gitignore b/MiniSpace.Services.MediaFiles/.gitignore new file mode 100644 index 00000000..6f04bbaa --- /dev/null +++ b/MiniSpace.Services.MediaFiles/.gitignore @@ -0,0 +1,332 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ +# **/Properties/launchSettings.json + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +logs/ \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/Dockerfile b/MiniSpace.Services.MediaFiles/Dockerfile new file mode 100644 index 00000000..17d4594f --- /dev/null +++ b/MiniSpace.Services.MediaFiles/Dockerfile @@ -0,0 +1,17 @@ +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /app + +COPY . . + +RUN dotnet publish src/MiniSpace.Services.MediaFiles.Api -c Release -o out + +FROM mcr.microsoft.com/dotnet/aspnet:8.0 +WORKDIR /app + +COPY --from=build /app/out . + +ENV ASPNETCORE_URLS=http://*:80 +ENV ASPNETCORE_ENVIRONMENT=docker +ENV NTRADA_CONFIG=ntrada.docker + +ENTRYPOINT ["dotnet", "MiniSpace.Services.MediaFiles.Api.dll"] diff --git a/MiniSpace.Services.MediaFiles/LICENSE b/MiniSpace.Services.MediaFiles/LICENSE new file mode 100644 index 00000000..b7ea7f0c --- /dev/null +++ b/MiniSpace.Services.MediaFiles/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 DevMentors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MiniSpace.Services.MediaFiles/MiniSpace.Services.MediaFiles.sln b/MiniSpace.Services.MediaFiles/MiniSpace.Services.MediaFiles.sln new file mode 100644 index 00000000..c008a6e7 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/MiniSpace.Services.MediaFiles.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{19970311-BECA-46BA-9763-C8CE5FBEC34C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.MediaFiles.Api", "src\MiniSpace.Services.MediaFiles.Api\MiniSpace.Services.MediaFiles.Api.csproj", "{E3332633-A8EA-47DD-95BE-2E4AF82A25A1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.MediaFiles.Application", "src\MiniSpace.Services.MediaFiles.Application\MiniSpace.Services.MediaFiles.Application.csproj", "{85A84271-21EA-4E82-8023-115BA562BF22}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.MediaFiles.Core", "src\MiniSpace.Services.MediaFiles.Core\MiniSpace.Services.MediaFiles.Core.csproj", "{DEED64BD-467A-4880-B078-C4E9FF257CB3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.MediaFiles.Infrastructure", "src\MiniSpace.Services.MediaFiles.Infrastructure\MiniSpace.Services.MediaFiles.Infrastructure.csproj", "{D8F4492A-C273-48FE-9611-1AC8E016944B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {E3332633-A8EA-47DD-95BE-2E4AF82A25A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E3332633-A8EA-47DD-95BE-2E4AF82A25A1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E3332633-A8EA-47DD-95BE-2E4AF82A25A1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E3332633-A8EA-47DD-95BE-2E4AF82A25A1}.Release|Any CPU.Build.0 = Release|Any CPU + {85A84271-21EA-4E82-8023-115BA562BF22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {85A84271-21EA-4E82-8023-115BA562BF22}.Debug|Any CPU.Build.0 = Debug|Any CPU + {85A84271-21EA-4E82-8023-115BA562BF22}.Release|Any CPU.ActiveCfg = Release|Any CPU + {85A84271-21EA-4E82-8023-115BA562BF22}.Release|Any CPU.Build.0 = Release|Any CPU + {DEED64BD-467A-4880-B078-C4E9FF257CB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DEED64BD-467A-4880-B078-C4E9FF257CB3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DEED64BD-467A-4880-B078-C4E9FF257CB3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DEED64BD-467A-4880-B078-C4E9FF257CB3}.Release|Any CPU.Build.0 = Release|Any CPU + {D8F4492A-C273-48FE-9611-1AC8E016944B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D8F4492A-C273-48FE-9611-1AC8E016944B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D8F4492A-C273-48FE-9611-1AC8E016944B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D8F4492A-C273-48FE-9611-1AC8E016944B}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {E3332633-A8EA-47DD-95BE-2E4AF82A25A1} = {19970311-BECA-46BA-9763-C8CE5FBEC34C} + {85A84271-21EA-4E82-8023-115BA562BF22} = {19970311-BECA-46BA-9763-C8CE5FBEC34C} + {DEED64BD-467A-4880-B078-C4E9FF257CB3} = {19970311-BECA-46BA-9763-C8CE5FBEC34C} + {D8F4492A-C273-48FE-9611-1AC8E016944B} = {19970311-BECA-46BA-9763-C8CE5FBEC34C} + EndGlobalSection +EndGlobal diff --git a/MiniSpace.Services.MediaFiles/scripts/build.sh b/MiniSpace.Services.MediaFiles/scripts/build.sh new file mode 100644 index 00000000..3affad0e --- /dev/null +++ b/MiniSpace.Services.MediaFiles/scripts/build.sh @@ -0,0 +1,2 @@ +#!/bin/bash +dotnet build -c release \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/scripts/dockerize-tag-push.sh b/MiniSpace.Services.MediaFiles/scripts/dockerize-tag-push.sh new file mode 100644 index 00000000..456d4e3c --- /dev/null +++ b/MiniSpace.Services.MediaFiles/scripts/dockerize-tag-push.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +export ASPNETCORE_ENVIRONMENT=docker + +cd .. + +docker build -t minispace.services.mediafiles:latest . + +docker tag minispace.services.mediafiles:latest adrianvsaint/minispace.services.mediafiles:latest + +docker push adrianvsaint/minispace.services.mediafiles:latest diff --git a/MiniSpace.Services.MediaFiles/scripts/dockerize.sh b/MiniSpace.Services.MediaFiles/scripts/dockerize.sh new file mode 100644 index 00000000..cfae9679 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/scripts/dockerize.sh @@ -0,0 +1,21 @@ +#!/bin/bash +TAG='' +VERSION_TAG= + +case "$TRAVIS_BRANCH" in + "master") + TAG=latest + VERSION_TAG=$TRAVIS_BUILD_NUMBER + ;; + "develop") + TAG=dev + VERSION_TAG=$TAG-$TRAVIS_BUILD_NUMBER + ;; +esac + +REPOSITORY=$DOCKER_USERNAME/minispace.services.mediafiles + +docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD +docker build -t $REPOSITORY:$TAG -t $REPOSITORY:$VERSION_TAG . +docker push $REPOSITORY:$TAG +docker push $REPOSITORY:$VERSION_TAG \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/scripts/start.sh b/MiniSpace.Services.MediaFiles/scripts/start.sh new file mode 100644 index 00000000..ba33d0a2 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/scripts/start.sh @@ -0,0 +1,4 @@ +#!/bin/bash +export ASPNETCORE_ENVIRONMENT=local +cd src/MiniSpace.Services.MediaFiles.Api +dotnet run diff --git a/MiniSpace.Services.MediaFiles/scripts/test.sh b/MiniSpace.Services.MediaFiles/scripts/test.sh new file mode 100644 index 00000000..6046c35a --- /dev/null +++ b/MiniSpace.Services.MediaFiles/scripts/test.sh @@ -0,0 +1,2 @@ +#!/bin/bash +dotnet test \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/MiniSpace.Services.MediaFiles.Api.csproj b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/MiniSpace.Services.MediaFiles.Api.csproj new file mode 100644 index 00000000..667a9e00 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/MiniSpace.Services.MediaFiles.Api.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + disable + enable + true + + + + + + + + + + + + + + diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/Program.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/Program.cs new file mode 100644 index 00000000..ea28dcc1 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/Program.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Convey; +using Convey.Logging; +using Convey.Types; +using Convey.WebApi; +using Convey.WebApi.CQRS; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using MiniSpace.Services.MediaFiles.Application; +using MiniSpace.Services.MediaFiles.Application.Commands; +using MiniSpace.Services.MediaFiles.Application.Dto; +using MiniSpace.Services.MediaFiles.Application.Queries; +using MiniSpace.Services.MediaFiles.Application.Services; +using MiniSpace.Services.MediaFiles.Infrastructure; + +namespace MiniSpace.Services.MediaFiles.Api +{ + public class Program + { + public static async Task Main(string[] args) + => await WebHost.CreateDefaultBuilder(args) + .ConfigureServices(services => services + .AddConvey() + .AddWebApi() + .AddApplication() + .AddInfrastructure() + .Build()) + .Configure(app => app + .UseInfrastructure() + .UseEndpoints(endpoints => endpoints + .Post("media-files", async (cmd, ctx) => + { + var fileId = await ctx.RequestServices.GetService().UploadAsync(cmd); + await ctx.Response.WriteJsonAsync(fileId); + }) + ) + .UseDispatcherEndpoints(endpoints => endpoints + .Get("", ctx => ctx.Response.WriteAsync(ctx.RequestServices.GetService().Name)) + .Get("media-files/{mediaFileId}") + .Get("media-files/{mediaFileId}/original") + .Delete("media-files/{mediaFileId}") + )) + .UseLogging() + .Build() + .RunAsync(); + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/Properties/launchSettings.json b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/Properties/launchSettings.json new file mode 100644 index 00000000..3387cd0a --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/Properties/launchSettings.json @@ -0,0 +1,26 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:5014" + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": false, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "local" + } + }, + "MiniSpace.Services.Events": { + "commandName": "Project", + "launchBrowser": false, + "applicationUrl": "http://localhost:5014", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "local" + } + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/appsettings.Development.json b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/appsettings.Development.json new file mode 100644 index 00000000..7a73a41b --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/appsettings.Development.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/appsettings.docker.json b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/appsettings.docker.json new file mode 100644 index 00000000..5d8e6992 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/appsettings.docker.json @@ -0,0 +1,153 @@ +{ + "app": { + "name": "MiniSpace Media Files Service", + "version": "1" + }, + "consul": { + "enabled": true, + "url": "http://consul:8500", + "service": "mediafiles-service", + "address": "mediafiles-service", + "port": "80", + "pingEnabled": true, + "pingEndpoint": "ping", + "pingInterval": 3, + "removeAfterInterval": 3 + }, + "fabio": { + "enabled": true, + "url": "http://fabio:9999", + "service": "mediafiles-service" + }, + "httpClient": { + "type": "fabio", + "retries": 3, + "services": {} + }, + "jwt": { + "certificate": { + "location": "", + "password": "", + "rawData": "" + }, + "issuerSigningKey": "eiquief5phee9pazo0Faegaez9gohThailiur5woy2befiech1oarai4aiLi6ahVecah3ie9Aiz6Peij", + "expiryMinutes": 60, + "issuer": "minispace", + "validateAudience": false, + "validateIssuer": false, + "validateLifetime": true, + "allowAnonymousEndpoints": ["/sign-in", "/sign-up"] + }, + "logger": { + "console": { + "enabled": true + }, + "elk": { + "enabled": false, + "url": "http://elk:9200" + }, + "file": { + "enabled": false, + "path": "logs/logs.txt", + "interval": "day" + }, + "seq": { + "enabled": true, + "url": "http://seq:5341", + "apiKey": "secret" + } + }, + "jaeger": { + "enabled": true, + "serviceName": "events", + "udpHost": "jaeger", + "udpPort": 6831, + "maxPacketSize": 0, + "sampler": "const", + "excludePaths": ["/", "/ping", "/metrics"] + }, + "metrics": { + "enabled": true, + "influxEnabled": false, + "prometheusEnabled": true, + "influxUrl": "http://influx:8086", + "database": "minispace", + "env": "docker", + "interval": 5 + }, + "mongo": { + "connectionString": "mongodb+srv://minispace-user:9vd6IxYWUuuqhzEH@cluster0.mmhq4pe.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0", + "database": "mediafiles-service", + "seed": false + }, + "rabbitMq": { + "connectionName": "mediafiles-service", + "retries": 3, + "retryInterval": 2, + "conventionsCasing": "snakeCase", + "logger": { + "enabled": true + }, + "username": "guest", + "password": "guest", + "virtualHost": "/", + "port": 5672, + "hostnames": [ + "rabbitmq" + ], + "requestedConnectionTimeout": "00:00:30", + "requestedHeartbeat": "00:01:00", + "socketReadTimeout": "00:00:30", + "socketWriteTimeout": "00:00:30", + "continuationTimeout": "00:00:20", + "handshakeContinuationTimeout": "00:00:10", + "networkRecoveryInterval": "00:00:05", + "exchange": { + "declare": true, + "durable": true, + "autoDelete": false, + "type": "topic", + "name": "events" + }, + "queue": { + "declare": true, + "durable": true, + "exclusive": false, + "autoDelete": false, + "template": "mediafiles-service/{{exchange}}.{{message}}" + }, + "context": { + "enabled": true, + "header": "message_context" + }, + "spanContextHeader": "span_context" + }, + "redis": { + "connectionString": "redis", + "instance": "mediafiles:" + }, + "swagger": { + "enabled": true, + "reDocEnabled": false, + "name": "v1", + "title": "API", + "version": "v1", + "routePrefix": "docs", + "includeSecurity": true + }, + "vault": { + "enabled": false, + "url": "http://vault:8200", + "kv": { + "enabled": false + }, + "pki": { + "enabled": false + }, + "lease": { + "mongo": { + "enabled": false + } + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/appsettings.json b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/appsettings.local.json b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/appsettings.local.json new file mode 100644 index 00000000..5647c8fd --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/appsettings.local.json @@ -0,0 +1,195 @@ +{ + "app": { + "name": "MiniSpace Media Files Service", + "service": "mediafiles-service", + "version": "1" + }, + "consul": { + "enabled": true, + "url": "http://localhost:8500", + "service": "mediafiles-service", + "address": "docker.for.win.localhost", + "port": "5014", + "pingEnabled": false, + "pingEndpoint": "ping", + "pingInterval": 3, + "removeAfterInterval": 3 + }, + "fabio": { + "enabled": true, + "url": "http://localhost:9999", + "service": "mediafiles-service" + }, + "httpClient": { + "type": "direct", + "retries": 3, + "services": { + }, + "requestMasking": { + "enabled": true, + "maskTemplate": "*****" + } + }, + "jwt": { + "issuerSigningKey": "eiquief5phee9pazo0Faegaez9gohThailiur5woy2befiech1oarai4aiLi6ahVecah3ie9Aiz6Peij", + "expiryMinutes": 60, + "issuer": "minispace", + "validateAudience": false, + "validateIssuer": false, + "validateLifetime": false, + "allowAnonymousEndpoints": ["/sign-in", "/sign-up"] + }, + "logger": { + "level": "information", + "excludePaths": ["/", "/ping", "/metrics"], + "excludeProperties": [ + "api_key", + "access_key", + "ApiKey", + "ApiSecret", + "ClientId", + "ClientSecret", + "ConnectionString", + "Password", + "Email", + "Login", + "Secret", + "Token" + ], + "console": { + "enabled": true + }, + "elk": { + "enabled": false, + "url": "http://localhost:9200" + }, + "file": { + "enabled": true, + "path": "logs/logs.txt", + "interval": "day" + }, + "seq": { + "enabled": true, + "url": "http://localhost:5341", + "apiKey": "secret" + }, + "tags": {} + }, + "jaeger": { + "enabled": true, + "serviceName": "mediafiles", + "udpHost": "localhost", + "udpPort": 6831, + "maxPacketSize": 0, + "sampler": "const", + "excludePaths": ["/", "/ping", "/metrics"] + }, + "metrics": { + "enabled": true, + "influxEnabled": false, + "prometheusEnabled": true, + "influxUrl": "http://localhost:8086", + "database": "minispace", + "env": "local", + "interval": 5 + }, + "mongo": { + "connectionString": "mongodb+srv://minispace-user:9vd6IxYWUuuqhzEH@cluster0.mmhq4pe.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0", + "database": "mediafiles-service", + "seed": false + }, + "outbox": { + "enabled": false, + "type": "sequential", + "expiry": 3600, + "intervalMilliseconds": 2000, + "inboxCollection": "inbox", + "outboxCollection": "outbox", + "disableTransactions": true + }, + "rabbitMq": { + "connectionName": "mediafiles-service", + "retries": 3, + "retryInterval": 2, + "conventionsCasing": "snakeCase", + "logger": { + "enabled": true + }, + "username": "guest", + "password": "guest", + "virtualHost": "/", + "port": 5672, + "hostnames": [ + "localhost" + ], + "requestedConnectionTimeout": "00:00:30", + "requestedHeartbeat": "00:01:00", + "socketReadTimeout": "00:00:30", + "socketWriteTimeout": "00:00:30", + "continuationTimeout": "00:00:20", + "handshakeContinuationTimeout": "00:00:10", + "networkRecoveryInterval": "00:00:05", + "exchange": { + "declare": true, + "durable": true, + "autoDelete": false, + "type": "topic", + "name": "mediafiles" + }, + "queue": { + "declare": true, + "durable": true, + "exclusive": false, + "autoDelete": false, + "template": "mediafiles-service/{{exchange}}.{{message}}" + }, + "context": { + "enabled": true, + "header": "message_context" + }, + "spanContextHeader": "span_context" + }, + "redis": { + "connectionString": "localhost", + "instance": "mediafiles:" + }, + "swagger": { + "enabled": true, + "reDocEnabled": false, + "name": "v1", + "title": "API", + "version": "v1", + "routePrefix": "docs", + "includeSecurity": true + }, + "vault": { + "enabled": true, + "url": "http://localhost:8200", + "authType": "token", + "token": "secret", + "username": "user", + "password": "secret", + "kv": { + "enabled": true, + "engineVersion": 2, + "mountPoint": "kv", + "path": "mediafiles-service/settings" + }, + "pki": { + "enabled": true, + "roleName": "mediafiles-service", + "commonName": "mediafiles-service.minispace.io" + }, + "lease": { + "mongo": { + "type": "database", + "roleName": "mediafiles-service", + "enabled": true, + "autoRenewal": true, + "templates": { + "connectionString": "mongodb://{{username}}:{{password}}@localhost:27017" + } + } + } + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Commands/CleanupUnassociatedFiles.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Commands/CleanupUnassociatedFiles.cs new file mode 100644 index 00000000..e97742e9 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Commands/CleanupUnassociatedFiles.cs @@ -0,0 +1,14 @@ +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.MediaFiles.Application.Commands +{ + public class CleanupUnassociatedFiles: ICommand + { + public DateTime Now { get; set; } + + public CleanupUnassociatedFiles(DateTime now) + { + Now = now; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Commands/DeleteMediaFile.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Commands/DeleteMediaFile.cs new file mode 100644 index 00000000..902fdefe --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Commands/DeleteMediaFile.cs @@ -0,0 +1,9 @@ +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.MediaFiles.Application.Commands +{ + public class DeleteMediaFile: ICommand + { + public Guid MediaFileId { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Commands/Handlers/CleanupUnassociatedFilesHandler.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Commands/Handlers/CleanupUnassociatedFilesHandler.cs new file mode 100644 index 00000000..53946e80 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Commands/Handlers/CleanupUnassociatedFilesHandler.cs @@ -0,0 +1,41 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.MediaFiles.Application.Events; +using MiniSpace.Services.MediaFiles.Application.Services; +using MiniSpace.Services.MediaFiles.Core.Entities; +using MiniSpace.Services.MediaFiles.Core.Repositories; + +namespace MiniSpace.Services.MediaFiles.Application.Commands.Handlers +{ + public class CleanupUnassociatedFilesHandler: ICommandHandler + { + private readonly IFileSourceInfoRepository _fileSourceInfoRepository; + private readonly IGridFSService _gridFSService; + private readonly IMessageBroker _messageBroker; + + public CleanupUnassociatedFilesHandler(IFileSourceInfoRepository fileSourceInfoRepository, IGridFSService gridFSService, + IMessageBroker messageBroker) + { + _fileSourceInfoRepository = fileSourceInfoRepository; + _gridFSService = gridFSService; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(CleanupUnassociatedFiles command, CancellationToken cancellationToken) + { + var unassociatedFileSourceInfos = await _fileSourceInfoRepository.GetAllUnassociatedAsync(); + foreach (var file in unassociatedFileSourceInfos) + { + if ((command.Now - file.CreatedAt).TotalDays < 1) + { + continue; + } + + await _gridFSService.DeleteFileAsync(file.OriginalFileId); + await _gridFSService.DeleteFileAsync(file.FileId); + await _fileSourceInfoRepository.DeleteAsync(file.Id); + } + + await _messageBroker.PublishAsync(new UnassociatedFilesCleaned(command.Now)); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Commands/Handlers/DeleteMediaFileHandler.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Commands/Handlers/DeleteMediaFileHandler.cs new file mode 100644 index 00000000..d3c005d4 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Commands/Handlers/DeleteMediaFileHandler.cs @@ -0,0 +1,46 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.MediaFiles.Application.Events; +using MiniSpace.Services.MediaFiles.Application.Exceptions; +using MiniSpace.Services.MediaFiles.Application.Services; +using MiniSpace.Services.MediaFiles.Core.Repositories; + +namespace MiniSpace.Services.MediaFiles.Application.Commands.Handlers +{ + public class DeleteMediaFileHandler: ICommandHandler + { + private readonly IFileSourceInfoRepository _fileSourceInfoRepository; + private readonly IGridFSService _gridFSService; + private readonly IAppContext _appContext; + private readonly IMessageBroker _messageBroker; + + public DeleteMediaFileHandler(IFileSourceInfoRepository fileSourceInfoRepository, IGridFSService gridFSService, + IAppContext appContext, IMessageBroker messageBroker) + { + _fileSourceInfoRepository = fileSourceInfoRepository; + _gridFSService = gridFSService; + _appContext = appContext; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(DeleteMediaFile command, CancellationToken cancellationToken) + { + var fileSourceInfo = await _fileSourceInfoRepository.GetAsync(command.MediaFileId); + if (fileSourceInfo is null) + { + throw new MediaFileNotFoundException(command.MediaFileId); + } + + var identity = _appContext.Identity; + if(identity.IsAuthenticated && identity.Id != fileSourceInfo.UploaderId && !identity.IsAdmin) + { + throw new UnauthorizedMediaFileAccessException(fileSourceInfo.Id, identity.Id, fileSourceInfo.UploaderId); + } + + await _gridFSService.DeleteFileAsync(fileSourceInfo.OriginalFileId); + await _gridFSService.DeleteFileAsync(fileSourceInfo.FileId); + await _fileSourceInfoRepository.DeleteAsync(command.MediaFileId); + await _messageBroker.PublishAsync(new MediaFileDeleted(command.MediaFileId, + fileSourceInfo.SourceId, fileSourceInfo.SourceType.ToString())); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Commands/UploadMediaFile.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Commands/UploadMediaFile.cs new file mode 100644 index 00000000..45937316 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Commands/UploadMediaFile.cs @@ -0,0 +1,28 @@ +using Convey.CQRS.Commands; +using Microsoft.AspNetCore.Http; + +namespace MiniSpace.Services.MediaFiles.Application.Commands +{ + public class UploadMediaFile : ICommand + { + public Guid MediaFileId { get; set; } + public Guid SourceId { get; set; } + public string SourceType { get; set; } + public Guid UploaderId { get; set; } + public string FileName { get; set; } + public string FileContentType { get; set; } + public string Base64Content { get; set; } + + public UploadMediaFile(Guid mediaFileId, Guid sourceId, string sourceType, Guid uploaderId, + string fileName, string fileContentType, string base64Content) + { + MediaFileId = mediaFileId == Guid.Empty ? Guid.NewGuid() : mediaFileId; + SourceId = sourceId; + SourceType = sourceType; + UploaderId = uploaderId; + FileName = fileName; + FileContentType = fileContentType; + Base64Content = base64Content; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/ContractAttribute.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/ContractAttribute.cs new file mode 100644 index 00000000..88c1e41a --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/ContractAttribute.cs @@ -0,0 +1,7 @@ +namespace MiniSpace.Services.MediaFiles.Application +{ + public class ContractAttribute : Attribute + { + + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Dto/FileDto.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Dto/FileDto.cs new file mode 100644 index 00000000..e794c5c3 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Dto/FileDto.cs @@ -0,0 +1,29 @@ +namespace MiniSpace.Services.MediaFiles.Application.Dto +{ + public class FileDto + { + public Guid MediaFileId { get; set; } + public Guid SourceId { get; set; } + public string SourceType { get; set; } + public string State { get; set; } + public DateTime CreatedAt { get; set; } + public Guid UploaderId { get; set; } + public string FileName { get; set; } + public string FileContentType { get; set; } + public string Base64Content { get; set; } + + public FileDto(Guid mediaFileId, Guid sourceId, string sourceType, Guid uploaderId, string state, + DateTime createdAt, string fileName, string fileContentType, string base64Content) + { + MediaFileId = mediaFileId; + SourceId = sourceId; + SourceType = sourceType; + UploaderId = uploaderId; + State = state; + CreatedAt = createdAt; + FileName = fileName; + FileContentType = fileContentType; + Base64Content = base64Content; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Dto/FileUploadResponseDto.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Dto/FileUploadResponseDto.cs new file mode 100644 index 00000000..257152cb --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Dto/FileUploadResponseDto.cs @@ -0,0 +1,12 @@ +namespace MiniSpace.Services.MediaFiles.Application.Dto +{ + public class FileUploadResponseDto + { + public Guid FileId { get; set; } + + public FileUploadResponseDto(Guid fileId) + { + FileId = fileId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/EventCreated.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/EventCreated.cs new file mode 100644 index 00000000..6bac7fdc --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/EventCreated.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.MediaFiles.Application.Events.External +{ + [Message("events")] + public class EventCreated : IEvent + { + public Guid EventId { get; } + public IEnumerable MediaFilesIds { get; } + + public EventCreated(Guid eventId, IEnumerable mediaFilesIds) + { + EventId = eventId; + MediaFilesIds = mediaFilesIds; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/EventDeleted.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/EventDeleted.cs new file mode 100644 index 00000000..a47fa908 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/EventDeleted.cs @@ -0,0 +1,16 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.MediaFiles.Application.Events.External +{ + [Message("events")] + public class EventDeleted : IEvent + { + public Guid EventId { get; } + + public EventDeleted(Guid eventId) + { + EventId = eventId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/EventUpdated.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/EventUpdated.cs new file mode 100644 index 00000000..b333bd6f --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/EventUpdated.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.MediaFiles.Application.Events.External +{ + [Message("events")] + public class EventUpdated : IEvent + { + public Guid EventId { get; } + public IEnumerable MediaFilesIds { get; } + + public EventUpdated(Guid eventId, IEnumerable mediaFilesIds) + { + EventId = eventId; + MediaFilesIds = mediaFilesIds; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/EventCreatedHandler.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/EventCreatedHandler.cs new file mode 100644 index 00000000..d27af87e --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/EventCreatedHandler.cs @@ -0,0 +1,32 @@ +using Convey.CQRS.Commands; +using Convey.CQRS.Events; +using MiniSpace.Services.MediaFiles.Application.Commands; +using MiniSpace.Services.MediaFiles.Core.Entities; +using MiniSpace.Services.MediaFiles.Core.Repositories; + +namespace MiniSpace.Services.MediaFiles.Application.Events.External.Handlers +{ + public class EventCreatedHandler : IEventHandler + { + private readonly IFileSourceInfoRepository _fileSourceInfoRepository; + + public EventCreatedHandler(IFileSourceInfoRepository fileSourceInfoRepository) + { + _fileSourceInfoRepository = fileSourceInfoRepository; + } + + public async Task HandleAsync(EventCreated @event, CancellationToken cancellationToken) + { + var fileSourceInfos = + await _fileSourceInfoRepository.FindAsync(@event.EventId, ContextType.Event); + foreach (var fileSourceInfo in fileSourceInfos) + { + if(@event.MediaFilesIds.Contains(fileSourceInfo.Id)) + { + fileSourceInfo.Associate(); + await _fileSourceInfoRepository.UpdateAsync(fileSourceInfo); + } + } + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/EventDeletedHandler.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/EventDeletedHandler.cs new file mode 100644 index 00000000..b007b0ae --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/EventDeletedHandler.cs @@ -0,0 +1,31 @@ +using Convey.CQRS.Commands; +using Convey.CQRS.Events; +using MiniSpace.Services.MediaFiles.Application.Commands; +using MiniSpace.Services.MediaFiles.Core.Entities; +using MiniSpace.Services.MediaFiles.Core.Repositories; + +namespace MiniSpace.Services.MediaFiles.Application.Events.External.Handlers +{ + public class EventDeletedHandler : IEventHandler + { + private readonly IFileSourceInfoRepository _fileSourceInfoRepository; + private readonly ICommandDispatcher _commandDispatcher; + + public EventDeletedHandler(IFileSourceInfoRepository fileSourceInfoRepository, ICommandDispatcher commandDispatcher) + { + _fileSourceInfoRepository = fileSourceInfoRepository; + _commandDispatcher = commandDispatcher; + } + + public async Task HandleAsync(EventDeleted @event, CancellationToken cancellationToken) + { + var fileSourceInfos = + await _fileSourceInfoRepository.FindAsync(@event.EventId, ContextType.Event); + foreach (var fileSourceInfo in fileSourceInfos) + { + fileSourceInfo.Unassociate(); + await _fileSourceInfoRepository.UpdateAsync(fileSourceInfo); + } + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/EventUpdatedHandler.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/EventUpdatedHandler.cs new file mode 100644 index 00000000..060861ca --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/EventUpdatedHandler.cs @@ -0,0 +1,38 @@ +using Convey.CQRS.Commands; +using Convey.CQRS.Events; +using MiniSpace.Services.MediaFiles.Application.Commands; +using MiniSpace.Services.MediaFiles.Core.Entities; +using MiniSpace.Services.MediaFiles.Core.Repositories; + +namespace MiniSpace.Services.MediaFiles.Application.Events.External.Handlers +{ + public class EventUpdatedHandler : IEventHandler + { + private readonly IFileSourceInfoRepository _fileSourceInfoRepository; + private readonly ICommandDispatcher _commandDispatcher; + + public EventUpdatedHandler(IFileSourceInfoRepository fileSourceInfoRepository, ICommandDispatcher commandDispatcher) + { + _fileSourceInfoRepository = fileSourceInfoRepository; + _commandDispatcher = commandDispatcher; + } + + public async Task HandleAsync(EventUpdated @event, CancellationToken cancellationToken) + { + var fileSourceInfos = + await _fileSourceInfoRepository.FindAsync(@event.EventId, ContextType.Event); + foreach (var fileSourceInfo in fileSourceInfos) + { + if(@event.MediaFilesIds.Contains(fileSourceInfo.Id)) + { + fileSourceInfo.Associate(); + } + else + { + fileSourceInfo.Unassociate(); + } + await _fileSourceInfoRepository.UpdateAsync(fileSourceInfo); + } + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/PostCreatedHandler.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/PostCreatedHandler.cs new file mode 100644 index 00000000..fa18b9ce --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/PostCreatedHandler.cs @@ -0,0 +1,34 @@ +using Convey.CQRS.Commands; +using Convey.CQRS.Events; +using MiniSpace.Services.MediaFiles.Application.Commands; +using MiniSpace.Services.MediaFiles.Core.Entities; +using MiniSpace.Services.MediaFiles.Core.Repositories; + +namespace MiniSpace.Services.MediaFiles.Application.Events.External.Handlers +{ + public class PostCreatedHandler : IEventHandler + { + private readonly IFileSourceInfoRepository _fileSourceInfoRepository; + private readonly ICommandDispatcher _commandDispatcher; + + public PostCreatedHandler(IFileSourceInfoRepository fileSourceInfoRepository, ICommandDispatcher commandDispatcher) + { + _fileSourceInfoRepository = fileSourceInfoRepository; + _commandDispatcher = commandDispatcher; + } + + public async Task HandleAsync(PostCreated @event, CancellationToken cancellationToken) + { + var fileSourceInfos = + await _fileSourceInfoRepository.FindAsync(@event.PostId, ContextType.Post); + foreach (var fileSourceInfo in fileSourceInfos) + { + if(@event.MediaFilesIds.Contains(fileSourceInfo.Id)) + { + fileSourceInfo.Associate(); + await _fileSourceInfoRepository.UpdateAsync(fileSourceInfo); + } + } + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/PostDeletedHandler.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/PostDeletedHandler.cs new file mode 100644 index 00000000..4c0c6a59 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/PostDeletedHandler.cs @@ -0,0 +1,31 @@ +using Convey.CQRS.Commands; +using Convey.CQRS.Events; +using MiniSpace.Services.MediaFiles.Application.Commands; +using MiniSpace.Services.MediaFiles.Core.Entities; +using MiniSpace.Services.MediaFiles.Core.Repositories; + +namespace MiniSpace.Services.MediaFiles.Application.Events.External.Handlers +{ + public class PostDeletedHandler : IEventHandler + { + private readonly IFileSourceInfoRepository _fileSourceInfoRepository; + private readonly ICommandDispatcher _commandDispatcher; + + public PostDeletedHandler(IFileSourceInfoRepository fileSourceInfoRepository, ICommandDispatcher commandDispatcher) + { + _fileSourceInfoRepository = fileSourceInfoRepository; + _commandDispatcher = commandDispatcher; + } + + public async Task HandleAsync(PostDeleted @event, CancellationToken cancellationToken) + { + var fileSourceInfos = + await _fileSourceInfoRepository.FindAsync(@event.PostId, ContextType.Post); + foreach (var fileSourceInfo in fileSourceInfos) + { + fileSourceInfo.Unassociate(); + await _fileSourceInfoRepository.UpdateAsync(fileSourceInfo); + } + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/PostUpdatedHandler.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/PostUpdatedHandler.cs new file mode 100644 index 00000000..7f7732ac --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/PostUpdatedHandler.cs @@ -0,0 +1,38 @@ +using Convey.CQRS.Commands; +using Convey.CQRS.Events; +using MiniSpace.Services.MediaFiles.Application.Commands; +using MiniSpace.Services.MediaFiles.Core.Entities; +using MiniSpace.Services.MediaFiles.Core.Repositories; + +namespace MiniSpace.Services.MediaFiles.Application.Events.External.Handlers +{ + public class PostUpdatedHandler : IEventHandler + { + private readonly IFileSourceInfoRepository _fileSourceInfoRepository; + private readonly ICommandDispatcher _commandDispatcher; + + public PostUpdatedHandler(IFileSourceInfoRepository fileSourceInfoRepository, ICommandDispatcher commandDispatcher) + { + _fileSourceInfoRepository = fileSourceInfoRepository; + _commandDispatcher = commandDispatcher; + } + + public async Task HandleAsync(PostUpdated @event, CancellationToken cancellationToken) + { + var fileSourceInfos = + await _fileSourceInfoRepository.FindAsync(@event.PostId, ContextType.Post); + foreach (var fileSourceInfo in fileSourceInfos) + { + if(@event.MediaFilesIds.Contains(fileSourceInfo.Id)) + { + fileSourceInfo.Associate(); + } + else + { + fileSourceInfo.Unassociate(); + } + await _fileSourceInfoRepository.UpdateAsync(fileSourceInfo); + } + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/StudentCreatedHandler.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/StudentCreatedHandler.cs new file mode 100644 index 00000000..0e1050a0 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/StudentCreatedHandler.cs @@ -0,0 +1,34 @@ +using Convey.CQRS.Commands; +using Convey.CQRS.Events; +using MiniSpace.Services.MediaFiles.Application.Commands; +using MiniSpace.Services.MediaFiles.Core.Entities; +using MiniSpace.Services.MediaFiles.Core.Repositories; + +namespace MiniSpace.Services.MediaFiles.Application.Events.External.Handlers +{ + public class StudentCreatedHandler : IEventHandler + { + private readonly IFileSourceInfoRepository _fileSourceInfoRepository; + private readonly ICommandDispatcher _commandDispatcher; + + public StudentCreatedHandler(IFileSourceInfoRepository fileSourceInfoRepository, ICommandDispatcher commandDispatcher) + { + _fileSourceInfoRepository = fileSourceInfoRepository; + _commandDispatcher = commandDispatcher; + } + + public async Task HandleAsync(StudentCreated @event, CancellationToken cancellationToken) + { + var fileSourceInfos = + await _fileSourceInfoRepository.FindAsync(@event.StudentId, ContextType.StudentProfile); + foreach (var fileSourceInfo in fileSourceInfos) + { + if (fileSourceInfo.Id == @event.MediaFileId) + { + fileSourceInfo.Associate(); + await _fileSourceInfoRepository.UpdateAsync(fileSourceInfo); + } + } + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/StudentDeletedHandler.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/StudentDeletedHandler.cs new file mode 100644 index 00000000..dfbf9cf7 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/StudentDeletedHandler.cs @@ -0,0 +1,31 @@ +using Convey.CQRS.Commands; +using Convey.CQRS.Events; +using MiniSpace.Services.MediaFiles.Application.Commands; +using MiniSpace.Services.MediaFiles.Core.Entities; +using MiniSpace.Services.MediaFiles.Core.Repositories; + +namespace MiniSpace.Services.MediaFiles.Application.Events.External.Handlers +{ + public class StudentDeletedHandler : IEventHandler + { + private readonly IFileSourceInfoRepository _fileSourceInfoRepository; + private readonly ICommandDispatcher _commandDispatcher; + + public StudentDeletedHandler(IFileSourceInfoRepository fileSourceInfoRepository, ICommandDispatcher commandDispatcher) + { + _fileSourceInfoRepository = fileSourceInfoRepository; + _commandDispatcher = commandDispatcher; + } + + public async Task HandleAsync(StudentDeleted @event, CancellationToken cancellationToken) + { + var fileSourceInfos = + await _fileSourceInfoRepository.FindAsync(@event.StudentId, ContextType.StudentProfile); + foreach (var fileSourceInfo in fileSourceInfos) + { + fileSourceInfo.Unassociate(); + await _fileSourceInfoRepository.UpdateAsync(fileSourceInfo); + } + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/StudentUpdatedHandler.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/StudentUpdatedHandler.cs new file mode 100644 index 00000000..e630a5f8 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/StudentUpdatedHandler.cs @@ -0,0 +1,38 @@ +using Convey.CQRS.Commands; +using Convey.CQRS.Events; +using MiniSpace.Services.MediaFiles.Application.Commands; +using MiniSpace.Services.MediaFiles.Core.Entities; +using MiniSpace.Services.MediaFiles.Core.Repositories; + +namespace MiniSpace.Services.MediaFiles.Application.Events.External.Handlers +{ + public class StudentUpdatedHandler : IEventHandler + { + private readonly IFileSourceInfoRepository _fileSourceInfoRepository; + private readonly ICommandDispatcher _commandDispatcher; + + public StudentUpdatedHandler(IFileSourceInfoRepository fileSourceInfoRepository, ICommandDispatcher commandDispatcher) + { + _fileSourceInfoRepository = fileSourceInfoRepository; + _commandDispatcher = commandDispatcher; + } + + public async Task HandleAsync(StudentUpdated @event, CancellationToken cancellationToken) + { + var fileSourceInfos = + await _fileSourceInfoRepository.FindAsync(@event.StudentId, ContextType.StudentProfile); + foreach (var fileSourceInfo in fileSourceInfos) + { + if (fileSourceInfo.Id == @event.MediaFileId) + { + fileSourceInfo.Associate(); + } + else + { + fileSourceInfo.Unassociate(); + } + await _fileSourceInfoRepository.UpdateAsync(fileSourceInfo); + } + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/PostCreated.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/PostCreated.cs new file mode 100644 index 00000000..2c54d7f2 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/PostCreated.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.MediaFiles.Application.Events.External +{ + [Message("posts")] + public class PostCreated : IEvent + { + public Guid PostId { get; } + public IEnumerable MediaFilesIds { get; } + + public PostCreated(Guid postId, IEnumerable mediaFilesIds) + { + PostId = postId; + MediaFilesIds = mediaFilesIds; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/PostDeleted.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/PostDeleted.cs new file mode 100644 index 00000000..210b1542 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/PostDeleted.cs @@ -0,0 +1,16 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.MediaFiles.Application.Events.External +{ + [Message("posts")] + public class PostDeleted : IEvent + { + public Guid PostId { get; } + + public PostDeleted(Guid postId) + { + PostId = postId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/PostUpdated.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/PostUpdated.cs new file mode 100644 index 00000000..a162cf8f --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/PostUpdated.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.MediaFiles.Application.Events.External +{ + [Message("posts")] + public class PostUpdated : IEvent + { + public Guid PostId { get; } + public IEnumerable MediaFilesIds { get; } + + public PostUpdated(Guid postId, IEnumerable mediaFilesIds) + { + PostId = postId; + MediaFilesIds = mediaFilesIds; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/StudentCreated.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/StudentCreated.cs new file mode 100644 index 00000000..0726ee12 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/StudentCreated.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.MediaFiles.Application.Events.External +{ + [Message("students")] + public class StudentCreated : IEvent + { + public Guid StudentId { get; } + public Guid MediaFileId { get; } + + public StudentCreated(Guid studentId, Guid mediaFileId) + { + StudentId = studentId; + MediaFileId = mediaFileId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/StudentDeleted.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/StudentDeleted.cs new file mode 100644 index 00000000..5c666e72 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/StudentDeleted.cs @@ -0,0 +1,16 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.MediaFiles.Application.Events.External +{ + [Message("students")] + public class StudentDeleted : IEvent + { + public Guid StudentId { get; } + + public StudentDeleted(Guid studentId) + { + StudentId = studentId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/StudentUpdated.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/StudentUpdated.cs new file mode 100644 index 00000000..43ebef22 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/StudentUpdated.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.MediaFiles.Application.Events.External +{ + [Message("students")] + public class StudentUpdated : IEvent + { + public Guid StudentId { get; } + public Guid MediaFileId { get; } + + public StudentUpdated(Guid studentId, Guid mediaFileId) + { + StudentId = studentId; + MediaFileId = mediaFileId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/FileCleanupBackgroundWorkerStarted.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/FileCleanupBackgroundWorkerStarted.cs new file mode 100644 index 00000000..4781c505 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/FileCleanupBackgroundWorkerStarted.cs @@ -0,0 +1,14 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.MediaFiles.Application.Events +{ + public class FileCleanupBackgroundWorkerStarted: IEvent + { + public DateTime StartedAt { get; } + + public FileCleanupBackgroundWorkerStarted(DateTime startedAt) + { + StartedAt = startedAt; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/FileCleanupBackgroundWorkerStopped.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/FileCleanupBackgroundWorkerStopped.cs new file mode 100644 index 00000000..5bcd641f --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/FileCleanupBackgroundWorkerStopped.cs @@ -0,0 +1,14 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.MediaFiles.Application.Events +{ + public class FileCleanupBackgroundWorkerStopped: IEvent + { + public DateTime StoppedAt { get; } + + public FileCleanupBackgroundWorkerStopped(DateTime stoppedAt) + { + StoppedAt = stoppedAt; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/MediaFileDeleted.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/MediaFileDeleted.cs new file mode 100644 index 00000000..a7eb41a8 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/MediaFileDeleted.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.MediaFiles.Application.Events +{ + public class MediaFileDeleted: IEvent + { + public Guid MediaFileId { get; } + public Guid SourceId { get; } + public string Source { get; } + + public MediaFileDeleted(Guid mediaFileId, Guid sourceId, string source) + { + MediaFileId = mediaFileId; + SourceId = sourceId; + Source = source; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/MediaFileUploaded.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/MediaFileUploaded.cs new file mode 100644 index 00000000..5f2cf96a --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/MediaFileUploaded.cs @@ -0,0 +1,16 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.MediaFiles.Application.Events +{ + public class MediaFileUploaded : IEvent + { + public Guid Id { get; } + public string FileName { get; } + + public MediaFileUploaded(Guid id, string fileName) + { + Id = id; + FileName = fileName; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/UnassociatedFilesCleaned.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/UnassociatedFilesCleaned.cs new file mode 100644 index 00000000..757821ca --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/UnassociatedFilesCleaned.cs @@ -0,0 +1,14 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.MediaFiles.Application.Events +{ + public class UnassociatedFilesCleaned: IEvent + { + public DateTime OccurredAt { get; } + + public UnassociatedFilesCleaned(DateTime occurredAt) + { + OccurredAt = occurredAt; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/AppException.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/AppException.cs new file mode 100644 index 00000000..4447155a --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/AppException.cs @@ -0,0 +1,11 @@ +namespace MiniSpace.Services.MediaFiles.Application.Exceptions +{ + public class AppException : Exception + { + public virtual string Code { get; } + + protected AppException(string message) : base(message) + { + } + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/FileTypeDoesNotMatchContentTypeException.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/FileTypeDoesNotMatchContentTypeException.cs new file mode 100644 index 00000000..94262359 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/FileTypeDoesNotMatchContentTypeException.cs @@ -0,0 +1,16 @@ +namespace MiniSpace.Services.MediaFiles.Application.Exceptions +{ + public class FileTypeDoesNotMatchContentTypeException : AppException + { + public override string Code { get; } = "file_type_does_not_match_content_type"; + public string FileType { get; } + public string ContentType { get; } + + public FileTypeDoesNotMatchContentTypeException(string fileType, string contentType) + : base($"File extension: {fileType} is not matching content type: {contentType}") + { + FileType = fileType; + ContentType = contentType; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/InvalidContextTypeException.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/InvalidContextTypeException.cs new file mode 100644 index 00000000..9efdbe12 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/InvalidContextTypeException.cs @@ -0,0 +1,13 @@ +namespace MiniSpace.Services.MediaFiles.Application.Exceptions +{ + public class InvalidContextTypeException : AppException + { + public override string Code { get; } = "invalid_context_type"; + public string ContextType { get; } + + public InvalidContextTypeException(string contextType) : base($"Invalid context type: {contextType}.") + { + ContextType = contextType; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/InvalidFileContentTypeException.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/InvalidFileContentTypeException.cs new file mode 100644 index 00000000..7afb2e5d --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/InvalidFileContentTypeException.cs @@ -0,0 +1,13 @@ +namespace MiniSpace.Services.MediaFiles.Application.Exceptions +{ + public class InvalidFileContentTypeException : AppException + { + public override string Code { get; } = "invalid_file_content_type"; + public string ContentType { get; } + + public InvalidFileContentTypeException(string contentType) : base($"Invalid file content type: {contentType}.") + { + ContentType = contentType; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/InvalidFileSizeException.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/InvalidFileSizeException.cs new file mode 100644 index 00000000..e570dec7 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/InvalidFileSizeException.cs @@ -0,0 +1,16 @@ +namespace MiniSpace.Services.MediaFiles.Application.Exceptions +{ + public class InvalidFileSizeException : AppException + { + public override string Code { get; } = "invalid_file_size"; + public int FileSize { get; } + public int MaxFileSize { get; } + + public InvalidFileSizeException(int fileSize, int maxFileSize) + : base($"Invalid file size: {fileSize}. Maximum valid file size: {maxFileSize}.") + { + FileSize = fileSize; + MaxFileSize = maxFileSize; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/MediaFileNotFoundException.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/MediaFileNotFoundException.cs new file mode 100644 index 00000000..9c9bd144 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/MediaFileNotFoundException.cs @@ -0,0 +1,14 @@ +namespace MiniSpace.Services.MediaFiles.Application.Exceptions +{ + public class MediaFileNotFoundException: AppException + { + public override string Code { get; } = "media_file_not_found"; + public Guid MediaFileId { get; } + + public MediaFileNotFoundException(Guid mediaFileId) + : base($"Media file with ID: {mediaFileId} was not found.") + { + MediaFileId = mediaFileId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/UnauthorizedMediaFileAccessException.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/UnauthorizedMediaFileAccessException.cs new file mode 100644 index 00000000..e0b0e939 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/UnauthorizedMediaFileAccessException.cs @@ -0,0 +1,18 @@ +namespace MiniSpace.Services.MediaFiles.Application.Exceptions +{ + public class UnauthorizedMediaFileAccessException: AppException + { + public override string Code { get; } = "unauthorized_media_file_access"; + public Guid MediaFileId { get; } + public Guid UserId { get; } + public Guid UploaderId { get; } + + public UnauthorizedMediaFileAccessException(Guid mediaFileId, Guid userId, Guid uploaderId) + : base($"User with ID: {userId} tried to access media file with ID: {mediaFileId} uploaded by user with ID: {uploaderId}.") + { + MediaFileId = mediaFileId; + UserId = userId; + UploaderId = uploaderId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/UnauthorizedMediaFileUploadException.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/UnauthorizedMediaFileUploadException.cs new file mode 100644 index 00000000..91ad23b0 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/UnauthorizedMediaFileUploadException.cs @@ -0,0 +1,16 @@ +namespace MiniSpace.Services.MediaFiles.Application.Exceptions +{ + public class UnauthorizedMediaFileUploadException : AppException + { + public override string Code { get; } = "unauthorized_media_file_upload"; + public Guid IdentityId { get; } + public Guid UploaderId { get; } + + public UnauthorizedMediaFileUploadException(Guid identityId, Guid uploaderId) + : base($"User with ID: {uploaderId} is not authorized to upload media files. Identity ID: {identityId}.") + { + IdentityId = identityId; + UploaderId = uploaderId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Extensions.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Extensions.cs new file mode 100644 index 00000000..6553ffba --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Extensions.cs @@ -0,0 +1,16 @@ +using Convey; +using Convey.CQRS.Commands; +using Convey.CQRS.Events; + +namespace MiniSpace.Services.MediaFiles.Application +{ + public static class Extensions + { + public static IConveyBuilder AddApplication(this IConveyBuilder builder) + => builder + .AddCommandHandlers() + .AddEventHandlers() + .AddInMemoryCommandDispatcher() + .AddInMemoryEventDispatcher(); + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/IAppContext.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/IAppContext.cs new file mode 100644 index 00000000..d3084031 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/IAppContext.cs @@ -0,0 +1,8 @@ +namespace MiniSpace.Services.MediaFiles.Application +{ + public interface IAppContext + { + string RequestId { get; } + IIdentityContext Identity { get; } + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/IIdentityContext.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/IIdentityContext.cs new file mode 100644 index 00000000..b0748b5e --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/IIdentityContext.cs @@ -0,0 +1,15 @@ +namespace MiniSpace.Services.MediaFiles.Application +{ + public interface IIdentityContext + { + Guid Id { get; } + string Role { get; } + string Name { get; } + string Email { get; } + bool IsAuthenticated { get; } + bool IsAdmin { get; } + bool IsBanned { get; } + bool IsOrganizer { get; } + IDictionary Claims { get; } + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/MiniSpace.Services.MediaFiles.Application.csproj b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/MiniSpace.Services.MediaFiles.Application.csproj new file mode 100644 index 00000000..a4867130 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/MiniSpace.Services.MediaFiles.Application.csproj @@ -0,0 +1,23 @@ + + + + net8.0 + enable + disable + + + + + + + + + + + + + + + + + diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Queries/GetMediaFile.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Queries/GetMediaFile.cs new file mode 100644 index 00000000..06ad3f32 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Queries/GetMediaFile.cs @@ -0,0 +1,11 @@ +using Convey.CQRS.Queries; +using Microsoft.AspNetCore.Mvc; +using MiniSpace.Services.MediaFiles.Application.Dto; + +namespace MiniSpace.Services.MediaFiles.Application.Queries +{ + public class GetMediaFile : IQuery + { + public Guid MediaFileId { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Queries/GetOriginalMediaFile.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Queries/GetOriginalMediaFile.cs new file mode 100644 index 00000000..09c9fdf6 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Queries/GetOriginalMediaFile.cs @@ -0,0 +1,10 @@ +using Convey.CQRS.Queries; +using MiniSpace.Services.MediaFiles.Application.Dto; + +namespace MiniSpace.Services.MediaFiles.Application.Queries +{ + public class GetOriginalMediaFile : IQuery + { + public Guid MediaFileId { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Services/IDateTimeProvider.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Services/IDateTimeProvider.cs new file mode 100644 index 00000000..9cb5354b --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Services/IDateTimeProvider.cs @@ -0,0 +1,7 @@ +namespace MiniSpace.Services.MediaFiles.Application.Services +{ + public interface IDateTimeProvider + { + DateTime Now { get; } + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Services/IEventMapper.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Services/IEventMapper.cs new file mode 100644 index 00000000..b26bb7e2 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Services/IEventMapper.cs @@ -0,0 +1,11 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.MediaFiles.Core.Events; + +namespace MiniSpace.Services.MediaFiles.Application.Services +{ + public interface IEventMapper + { + IEvent Map(IDomainEvent @event); + IEnumerable MapAll(IEnumerable events); + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Services/IFileValidator.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Services/IFileValidator.cs new file mode 100644 index 00000000..b6450def --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Services/IFileValidator.cs @@ -0,0 +1,8 @@ +namespace MiniSpace.Services.MediaFiles.Application.Services +{ + public interface IFileValidator + { + public void ValidateFileSize(int size); + public void ValidateFileExtensions(byte[] bytes, string contentType); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Services/IGridFSService.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Services/IGridFSService.cs new file mode 100644 index 00000000..0f40c210 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Services/IGridFSService.cs @@ -0,0 +1,11 @@ +using MongoDB.Bson; + +namespace MiniSpace.Services.MediaFiles.Application.Services +{ + public interface IGridFSService + { + Task UploadFileAsync(string fileName, Stream fileStream); + Task DownloadFileAsync(ObjectId fileId, Stream destination); + Task DeleteFileAsync(ObjectId fileId); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Services/IMediaFilesService.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Services/IMediaFilesService.cs new file mode 100644 index 00000000..1a3893fd --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Services/IMediaFilesService.cs @@ -0,0 +1,10 @@ +using MiniSpace.Services.MediaFiles.Application.Commands; +using MiniSpace.Services.MediaFiles.Application.Dto; + +namespace MiniSpace.Services.MediaFiles.Application.Services +{ + public interface IMediaFilesService + { + public Task UploadAsync(UploadMediaFile command); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Services/IMessageBroker.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Services/IMessageBroker.cs new file mode 100644 index 00000000..2af11730 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Services/IMessageBroker.cs @@ -0,0 +1,10 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.MediaFiles.Application.Services +{ + public interface IMessageBroker + { + Task PublishAsync(params IEvent[] events); + Task PublishAsync(IEnumerable events); + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Entities/AggregateId.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Entities/AggregateId.cs new file mode 100644 index 00000000..92249149 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Entities/AggregateId.cs @@ -0,0 +1,50 @@ +using MiniSpace.Services.MediaFiles.Core.Exceptions; + +namespace MiniSpace.Services.MediaFiles.Core.Entities +{ + public class AggregateId : IEquatable + { + public Guid Value { get; } + + public AggregateId() + { + Value = Guid.NewGuid(); + } + + public AggregateId(Guid value) + { + if (value == Guid.Empty) + { + throw new InvalidAggregateIdException(); + } + + Value = value; + } + + public bool Equals(AggregateId other) + { + if (ReferenceEquals(null, other)) return false; + return ReferenceEquals(this, other) || Value.Equals(other.Value); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + return obj.GetType() == GetType() && Equals((AggregateId) obj); + } + + public override int GetHashCode() + { + return Value.GetHashCode(); + } + + public static implicit operator Guid(AggregateId id) + => id.Value; + + public static implicit operator AggregateId(Guid id) + => new AggregateId(id); + + public override string ToString() => Value.ToString(); + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Entities/AggregateRoot.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Entities/AggregateRoot.cs new file mode 100644 index 00000000..fca21a6d --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Entities/AggregateRoot.cs @@ -0,0 +1,19 @@ +using MiniSpace.Services.MediaFiles.Core.Events; + +namespace MiniSpace.Services.MediaFiles.Core.Entities +{ + public abstract class AggregateRoot + { + private readonly List _events = new List(); + public IEnumerable Events => _events; + public AggregateId Id { get; protected set; } + public int Version { get; protected set; } + + protected void AddEvent(IDomainEvent @event) + { + _events.Add(@event); + } + + public void ClearEvents() => _events.Clear(); + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Entities/ContextType.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Entities/ContextType.cs new file mode 100644 index 00000000..c93a71bd --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Entities/ContextType.cs @@ -0,0 +1,9 @@ +namespace MiniSpace.Services.MediaFiles.Core.Entities +{ + public enum ContextType + { + Event, + Post, + StudentProfile, + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Entities/FileSourceInfo.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Entities/FileSourceInfo.cs new file mode 100644 index 00000000..84e52e5c --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Entities/FileSourceInfo.cs @@ -0,0 +1,42 @@ +using MongoDB.Bson; + +namespace MiniSpace.Services.MediaFiles.Core.Entities +{ + public class FileSourceInfo: AggregateRoot + { + public Guid SourceId { get; set; } + public ContextType SourceType { get; set; } + public Guid UploaderId { get; set; } + public State State { get; set; } + public DateTime CreatedAt { get; set; } + public ObjectId OriginalFileId { get; set; } + public string OriginalFileContentType { get; set; } + public ObjectId FileId { get; set; } + public string FileName { get; set; } + + public FileSourceInfo(Guid id, Guid sourceId, ContextType sourceType, Guid uploaderId, State state, + DateTime createdAt, ObjectId originalFileId, string originalFileContentType, ObjectId fileId, string fileName) + { + Id = id; + SourceId = sourceId; + SourceType = sourceType; + UploaderId = uploaderId; + State = state; + CreatedAt = createdAt; + OriginalFileId = originalFileId; + OriginalFileContentType = originalFileContentType; + FileId = fileId; + FileName = fileName; + } + + public void Associate() + { + State = State.Associated; + } + + public void Unassociate() + { + State = State.Unassociated; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Entities/State.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Entities/State.cs new file mode 100644 index 00000000..45aa5561 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Entities/State.cs @@ -0,0 +1,8 @@ +namespace MiniSpace.Services.MediaFiles.Core.Entities +{ + public enum State + { + Associated, + Unassociated + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Events/IDomainEvent.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Events/IDomainEvent.cs new file mode 100644 index 00000000..ecf21afe --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Events/IDomainEvent.cs @@ -0,0 +1,7 @@ +namespace MiniSpace.Services.MediaFiles.Core.Events +{ + public interface IDomainEvent + { + + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Exceptions/DomainException.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Exceptions/DomainException.cs new file mode 100644 index 00000000..cc1f1963 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Exceptions/DomainException.cs @@ -0,0 +1,11 @@ +namespace MiniSpace.Services.MediaFiles.Core.Exceptions +{ + public abstract class DomainException : Exception + { + public virtual string Code { get; } + + protected DomainException(string message) : base(message) + { + } + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Exceptions/InvalidAggregateIdException.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Exceptions/InvalidAggregateIdException.cs new file mode 100644 index 00000000..376f91b1 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Exceptions/InvalidAggregateIdException.cs @@ -0,0 +1,11 @@ +namespace MiniSpace.Services.MediaFiles.Core.Exceptions +{ + public class InvalidAggregateIdException : DomainException + { + public override string Code { get; } = "invalid_aggregate_id"; + + public InvalidAggregateIdException() : base($"Invalid aggregate id.") + { + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/MiniSpace.Services.MediaFiles.Core.csproj b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/MiniSpace.Services.MediaFiles.Core.csproj new file mode 100644 index 00000000..4e485b55 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/MiniSpace.Services.MediaFiles.Core.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + disable + + + + + + + diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Repositories/IFileSourceInfoRepository.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Repositories/IFileSourceInfoRepository.cs new file mode 100644 index 00000000..5095eadc --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Repositories/IFileSourceInfoRepository.cs @@ -0,0 +1,15 @@ +using MiniSpace.Services.MediaFiles.Core.Entities; + +namespace MiniSpace.Services.MediaFiles.Core.Repositories +{ + public interface IFileSourceInfoRepository + { + Task GetAsync(Guid id); + Task> GetAllUnassociatedAsync(); + Task AddAsync(FileSourceInfo fileSourceInfo); + Task UpdateAsync(FileSourceInfo fileSourceInfo); + Task DeleteAsync(Guid id); + Task ExistsAsync(Guid id); + Task> FindAsync(Guid sourceId, ContextType sourceType); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Contexts/AppContext.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Contexts/AppContext.cs new file mode 100644 index 00000000..7d9aa35b --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Contexts/AppContext.cs @@ -0,0 +1,27 @@ +using MiniSpace.Services.MediaFiles.Application; + +namespace MiniSpace.Services.MediaFiles.Infrastructure.Contexts +{ + internal class AppContext : IAppContext + { + public string RequestId { get; } + public IIdentityContext Identity { get; } + + internal AppContext() : this(Guid.NewGuid().ToString("N"), IdentityContext.Empty) + { + } + + internal AppContext(CorrelationContext context) : this(context.CorrelationId, + context.User is null ? IdentityContext.Empty : new IdentityContext(context.User)) + { + } + + internal AppContext(string requestId, IIdentityContext identity) + { + RequestId = requestId; + Identity = identity; + } + + internal static IAppContext Empty => new AppContext(); + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Contexts/AppContextFactory.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Contexts/AppContextFactory.cs new file mode 100644 index 00000000..db1770db --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Contexts/AppContextFactory.cs @@ -0,0 +1,35 @@ +using Convey.MessageBrokers; +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; +using MiniSpace.Services.MediaFiles.Application; + +namespace MiniSpace.Services.MediaFiles.Infrastructure.Contexts +{ + internal sealed class AppContextFactory : IAppContextFactory + { + private readonly ICorrelationContextAccessor _contextAccessor; + private readonly IHttpContextAccessor _httpContextAccessor; + + public AppContextFactory(ICorrelationContextAccessor contextAccessor, IHttpContextAccessor httpContextAccessor) + { + _contextAccessor = contextAccessor; + _httpContextAccessor = httpContextAccessor; + } + + public IAppContext Create() + { + if (_contextAccessor.CorrelationContext is { }) + { + var payload = JsonConvert.SerializeObject(_contextAccessor.CorrelationContext); + + return string.IsNullOrWhiteSpace(payload) + ? AppContext.Empty + : new AppContext(JsonConvert.DeserializeObject(payload)); + } + + var context = _httpContextAccessor.GetCorrelationContext(); + + return context is null ? AppContext.Empty : new AppContext(context); + } + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Contexts/CorrelationContext.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Contexts/CorrelationContext.cs new file mode 100644 index 00000000..1ba6bbba --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Contexts/CorrelationContext.cs @@ -0,0 +1,22 @@ +namespace MiniSpace.Services.MediaFiles.Infrastructure.Contexts +{ + internal class CorrelationContext + { + public string CorrelationId { get; set; } + public string SpanContext { get; set; } + public UserContext User { get; set; } + public string ResourceId { get; set; } + public string TraceId { get; set; } + public string ConnectionId { get; set; } + public string Name { get; set; } + public DateTime CreatedAt { get; set; } + + public class UserContext + { + public string Id { get; set; } + public bool IsAuthenticated { get; set; } + public string Role { get; set; } + public IDictionary Claims { get; set; } + } + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Contexts/IdentityContext.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Contexts/IdentityContext.cs new file mode 100644 index 00000000..10515178 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Contexts/IdentityContext.cs @@ -0,0 +1,41 @@ +using MiniSpace.Services.MediaFiles.Application; + +namespace MiniSpace.Services.MediaFiles.Infrastructure.Contexts +{ + internal class IdentityContext : IIdentityContext + { + public Guid Id { get; } + public string Role { get; } = string.Empty; + public string Name { get; } = string.Empty; + public string Email { get; } = string.Empty; + public bool IsAuthenticated { get; } + public bool IsAdmin { get; } + public bool IsBanned { get; } + public bool IsOrganizer { get; } + public IDictionary Claims { get; } = new Dictionary(); + + internal IdentityContext() + { + } + + internal IdentityContext(CorrelationContext.UserContext context) + : this(context.Id, context.Role, context.IsAuthenticated, context.Claims) + { + } + + internal IdentityContext(string id, string role, bool isAuthenticated, IDictionary claims) + { + Id = Guid.TryParse(id, out var userId) ? userId : Guid.Empty; + Role = role ?? string.Empty; + IsAuthenticated = isAuthenticated; + IsAdmin = Role.Equals("admin", StringComparison.InvariantCultureIgnoreCase); + IsBanned = Role.Equals("banned", StringComparison.InvariantCultureIgnoreCase); + IsOrganizer = Role.Equals("organizer", StringComparison.InvariantCultureIgnoreCase); + Claims = claims ?? new Dictionary(); + Name = Claims.TryGetValue("name", out var name) ? name : string.Empty; + Email = Claims.TryGetValue("email", out var email) ? email : string.Empty; + } + + internal static IIdentityContext Empty => new IdentityContext(); + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Decorators/OutboxCommandHandlerDecorator.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Decorators/OutboxCommandHandlerDecorator.cs new file mode 100644 index 00000000..cce8e588 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Decorators/OutboxCommandHandlerDecorator.cs @@ -0,0 +1,35 @@ +using Convey.CQRS.Commands; +using Convey.MessageBrokers; +using Convey.MessageBrokers.Outbox; +using Convey.Types; + +namespace MiniSpace.Services.MediaFiles.Infrastructure.Decorators +{ + [Decorator] + internal sealed class OutboxCommandHandlerDecorator : ICommandHandler + where TCommand : class, ICommand + { + private readonly ICommandHandler _handler; + private readonly IMessageOutbox _outbox; + private readonly string _messageId; + private readonly bool _enabled; + + public OutboxCommandHandlerDecorator(ICommandHandler handler, IMessageOutbox outbox, + OutboxOptions outboxOptions, IMessagePropertiesAccessor messagePropertiesAccessor) + { + _handler = handler; + _outbox = outbox; + _enabled = outboxOptions.Enabled; + + var messageProperties = messagePropertiesAccessor.MessageProperties; + _messageId = string.IsNullOrWhiteSpace(messageProperties?.MessageId) + ? Guid.NewGuid().ToString("N") + : messageProperties.MessageId; + } + + public Task HandleAsync(TCommand command, CancellationToken cancellationToken) + => _enabled + ? _outbox.HandleAsync(_messageId, () => _handler.HandleAsync(command)) + : _handler.HandleAsync(command); + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Decorators/OutboxEventHandlerDecorator.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Decorators/OutboxEventHandlerDecorator.cs new file mode 100644 index 00000000..2fe4847f --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Decorators/OutboxEventHandlerDecorator.cs @@ -0,0 +1,35 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; +using Convey.MessageBrokers.Outbox; +using Convey.Types; + +namespace MiniSpace.Services.MediaFiles.Infrastructure.Decorators +{ + [Decorator] + internal sealed class OutboxEventHandlerDecorator : IEventHandler + where TEvent : class, IEvent + { + private readonly IEventHandler _handler; + private readonly IMessageOutbox _outbox; + private readonly string _messageId; + private readonly bool _enabled; + + public OutboxEventHandlerDecorator(IEventHandler handler, IMessageOutbox outbox, + OutboxOptions outboxOptions, IMessagePropertiesAccessor messagePropertiesAccessor) + { + _handler = handler; + _outbox = outbox; + _enabled = outboxOptions.Enabled; + + var messageProperties = messagePropertiesAccessor.MessageProperties; + _messageId = string.IsNullOrWhiteSpace(messageProperties?.MessageId) + ? Guid.NewGuid().ToString("N") + : messageProperties.MessageId; + } + + public Task HandleAsync(TEvent @event, CancellationToken cancellationToken) + => _enabled + ? _outbox.HandleAsync(_messageId, () => _handler.HandleAsync(@event)) + : _handler.HandleAsync(@event); + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Exceptions/ExceptionToMessageMapper.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Exceptions/ExceptionToMessageMapper.cs new file mode 100644 index 00000000..da84e463 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Exceptions/ExceptionToMessageMapper.cs @@ -0,0 +1,14 @@ +using Convey.MessageBrokers.RabbitMQ; + +namespace MiniSpace.Services.MediaFiles.Infrastructure.Exceptions +{ + internal sealed class ExceptionToMessageMapper : IExceptionToMessageMapper + { + public object Map(Exception exception, object message) + => exception switch + + { + _ => null + }; + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Exceptions/ExceptionToResponseMapper.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Exceptions/ExceptionToResponseMapper.cs new file mode 100644 index 00000000..48a0dab9 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Exceptions/ExceptionToResponseMapper.cs @@ -0,0 +1,46 @@ +using System.Collections.Concurrent; +using System.Net; +using Convey; +using Convey.WebApi.Exceptions; +using MiniSpace.Services.MediaFiles.Application.Exceptions; +using MiniSpace.Services.MediaFiles.Core.Exceptions; + +namespace MiniSpace.Services.MediaFiles.Infrastructure.Exceptions +{ + internal sealed class ExceptionToResponseMapper : IExceptionToResponseMapper + { + private static readonly ConcurrentDictionary Codes = new ConcurrentDictionary(); + + public ExceptionResponse Map(Exception exception) + => exception switch + { + DomainException ex => new ExceptionResponse(new {code = GetCode(ex), reason = ex.Message}, + HttpStatusCode.BadRequest), + AppException ex => new ExceptionResponse(new {code = GetCode(ex), reason = ex.Message}, + HttpStatusCode.BadRequest), + _ => new ExceptionResponse(new {code = "error", reason = "There was an error."}, + HttpStatusCode.BadRequest) + }; + + private static string GetCode(Exception exception) + { + var type = exception.GetType(); + if (Codes.TryGetValue(type, out var code)) + { + return code; + } + + var exceptionCode = exception switch + { + DomainException domainException when !string.IsNullOrWhiteSpace(domainException.Code) => domainException + .Code, + AppException appException when !string.IsNullOrWhiteSpace(appException.Code) => appException.Code, + _ => exception.GetType().Name.Underscore().Replace("_exception", string.Empty) + }; + + Codes.TryAdd(type, exceptionCode); + + return exceptionCode; + } + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Extensions.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Extensions.cs new file mode 100644 index 00000000..0cec9be9 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Extensions.cs @@ -0,0 +1,147 @@ +using System.Text; +using Convey; +using Convey.CQRS.Commands; +using Convey.CQRS.Events; +using Convey.CQRS.Queries; +using Convey.Discovery.Consul; +using Convey.Docs.Swagger; +using Convey.HTTP; +using Convey.LoadBalancing.Fabio; +using Convey.MessageBrokers; +using Convey.MessageBrokers.CQRS; +using Convey.MessageBrokers.Outbox; +using Convey.MessageBrokers.Outbox.Mongo; +using Convey.MessageBrokers.RabbitMQ; +using Convey.Metrics.AppMetrics; +using Convey.Persistence.MongoDB; +using Convey.Persistence.Redis; +using Convey.Security; +using Convey.Tracing.Jaeger; +using Convey.Tracing.Jaeger.RabbitMQ; +using Convey.WebApi; +using Convey.WebApi.CQRS; +using Convey.WebApi.Security; +using Convey.WebApi.Swagger; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; +using MiniSpace.Services.MediaFiles.Application; +using MiniSpace.Services.MediaFiles.Application.Commands; +using MiniSpace.Services.MediaFiles.Application.Events.External; +using MiniSpace.Services.MediaFiles.Application.Services; +using MiniSpace.Services.MediaFiles.Core.Repositories; +using MiniSpace.Services.MediaFiles.Infrastructure.Contexts; +using MiniSpace.Services.MediaFiles.Infrastructure.Decorators; +using MiniSpace.Services.MediaFiles.Infrastructure.Exceptions; +using MiniSpace.Services.MediaFiles.Infrastructure.Logging; +using MiniSpace.Services.MediaFiles.Infrastructure.Mongo.Documents; +using MiniSpace.Services.MediaFiles.Infrastructure.Mongo.Repositories; +using MiniSpace.Services.MediaFiles.Infrastructure.Services; +using MiniSpace.Services.MediaFiles.Infrastructure.Services.Workers; +using MongoDB.Driver; + +namespace MiniSpace.Services.MediaFiles.Infrastructure +{ + public static class Extensions + { + public static IConveyBuilder AddInfrastructure(this IConveyBuilder builder) + { + builder.Services.AddTransient(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(ctx => ctx.GetRequiredService().Create()); + builder.Services.TryDecorate(typeof(ICommandHandler<>), typeof(OutboxCommandHandlerDecorator<>)); + builder.Services.TryDecorate(typeof(IEventHandler<>), typeof(OutboxEventHandlerDecorator<>)); + builder.Services.AddSingleton(serviceProvider => + { + var mongoDbOptions = serviceProvider.GetRequiredService(); + var mongoClient = new MongoClient(mongoDbOptions.ConnectionString); + var database = mongoClient.GetDatabase(mongoDbOptions.Database); + return new GridFSService(database); + }); + builder.Services.AddHostedService(); + + return builder + .AddErrorHandler() + .AddQueryHandlers() + .AddInMemoryQueryDispatcher() + .AddHttpClient() + .AddConsul() + .AddFabio() + .AddRabbitMq(plugins: p => p.AddJaegerRabbitMqPlugin()) + .AddMessageOutbox(o => o.AddMongo()) + .AddExceptionToMessageMapper() + .AddMongo() + .AddRedis() + .AddMetrics() + .AddJaeger() + .AddHandlersLogging() + .AddMongoRepository("fileSourceInfos") + .AddWebApiSwaggerDocs() + .AddCertificateAuthentication() + .AddSecurity(); + } + + public static IApplicationBuilder UseInfrastructure(this IApplicationBuilder app) + { + app.UseErrorHandler() + .UseSwaggerDocs() + .UseJaeger() + .UseConvey() + .UsePublicContracts() + .UseMetrics() + .UseCertificateAuthentication() + .UseRabbitMq() + .SubscribeCommand() + .SubscribeCommand() + .SubscribeCommand() + .SubscribeEvent() + .SubscribeEvent() + .SubscribeEvent() + .SubscribeEvent(); + + return app; + } + + internal static CorrelationContext GetCorrelationContext(this IHttpContextAccessor accessor) + => accessor.HttpContext?.Request.Headers.TryGetValue("Correlation-Context", out var json) is true + ? JsonConvert.DeserializeObject(json.FirstOrDefault()) + : null; + + internal static IDictionary GetHeadersToForward(this IMessageProperties messageProperties) + { + const string sagaHeader = "Saga"; + if (messageProperties?.Headers is null || !messageProperties.Headers.TryGetValue(sagaHeader, out var saga)) + { + return null; + } + + return saga is null + ? null + : new Dictionary + { + [sagaHeader] = saga + }; + } + + internal static string GetSpanContext(this IMessageProperties messageProperties, string header) + { + if (messageProperties is null) + { + return string.Empty; + } + + if (messageProperties.Headers.TryGetValue(header, out var span) && span is byte[] spanBytes) + { + return Encoding.UTF8.GetString(spanBytes); + } + + return string.Empty; + } + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/IAppContextFactory.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/IAppContextFactory.cs new file mode 100644 index 00000000..f6a451f1 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/IAppContextFactory.cs @@ -0,0 +1,9 @@ +using MiniSpace.Services.MediaFiles.Application; + +namespace MiniSpace.Services.MediaFiles.Infrastructure +{ + public interface IAppContextFactory + { + IAppContext Create(); + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Logging/Extensions.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Logging/Extensions.cs new file mode 100644 index 00000000..e358df03 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Logging/Extensions.cs @@ -0,0 +1,21 @@ +using Convey; +using Convey.Logging.CQRS; +using Microsoft.Extensions.DependencyInjection; +using MiniSpace.Services.MediaFiles.Application.Commands; + +namespace MiniSpace.Services.MediaFiles.Infrastructure.Logging +{ + internal static class Extensions + { + public static IConveyBuilder AddHandlersLogging(this IConveyBuilder builder) + { + var assembly = typeof(UploadMediaFile).Assembly; + + builder.Services.AddSingleton(new MessageToLogTemplateMapper()); + + return builder + .AddCommandHandlersLogging(assembly) + .AddEventHandlersLogging(assembly); + } + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Logging/MessageToLogTemplateMapper.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Logging/MessageToLogTemplateMapper.cs new file mode 100644 index 00000000..70696078 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Logging/MessageToLogTemplateMapper.cs @@ -0,0 +1,102 @@ +using Convey.Logging.CQRS; +using MiniSpace.Services.MediaFiles.Application.Commands; +using MiniSpace.Services.MediaFiles.Application.Events.External; + +namespace MiniSpace.Services.MediaFiles.Infrastructure.Logging +{ + internal sealed class MessageToLogTemplateMapper : IMessageToLogTemplateMapper + { + private static IReadOnlyDictionary MessageTemplates + => new Dictionary + { + { + typeof(UploadMediaFile), new HandlerLogTemplate + { + After = "Uploaded media file with ID: {MediaFileId} and name: {FileName}.", + } + }, + { + typeof(DeleteMediaFile), new HandlerLogTemplate + { + After = "Deleted media file with ID: {MediaFileId}.", + } + }, + { + typeof(StudentCreated), + new HandlerLogTemplate + { + After = "Associated profile picture with ID: {MediaFileId} for student with ID: {StudentId}.", + } + }, + { + typeof(StudentUpdated), + new HandlerLogTemplate + { + After = "Associated profile picture with ID: {MediaFileId} for student with ID: {StudentId}.", + } + }, + { + typeof(StudentDeleted), + new HandlerLogTemplate + { + After = "Deleted all media files for student with ID: {StudentId}.", + } + }, + { + typeof(PostCreated), + new HandlerLogTemplate + { + After = "Associated media files for post with ID: {PostId}.", + } + }, + { + typeof(PostUpdated), + new HandlerLogTemplate + { + After = "Associated media files for post with ID: {PostId}.", + } + }, + { + typeof(PostDeleted), + new HandlerLogTemplate + { + After = "Deleted all media files for post with ID: {PostId}.", + } + }, + { + typeof(EventCreated), + new HandlerLogTemplate + { + After = "Associated media files for event with ID: {EventId}.", + } + }, + { + typeof(EventUpdated), + new HandlerLogTemplate + { + After = "Associated media files for event with ID: {EventId}.", + } + }, + { + typeof(EventDeleted), + new HandlerLogTemplate + { + After = "Deleted all media files for event with ID: {EventId}.", + } + }, + { + typeof(CleanupUnassociatedFiles), + new HandlerLogTemplate + { + After = "Cleaned unmatched files for all entities at {Now}." + } + }, + }; + + public HandlerLogTemplate Map(TMessage message) where TMessage : class + { + var key = message.GetType(); + return MessageTemplates.TryGetValue(key, out var template) ? template : null; + } + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/MiniSpace.Services.MediaFiles.Infrastructure.csproj b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/MiniSpace.Services.MediaFiles.Infrastructure.csproj new file mode 100644 index 00000000..cbe16ffa --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/MiniSpace.Services.MediaFiles.Infrastructure.csproj @@ -0,0 +1,40 @@ + + + + net8.0 + enable + disable + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Mongo/Documents/Extensions.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Mongo/Documents/Extensions.cs new file mode 100644 index 00000000..ca41d053 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Mongo/Documents/Extensions.cs @@ -0,0 +1,27 @@ +using MiniSpace.Services.MediaFiles.Core.Entities; + +namespace MiniSpace.Services.MediaFiles.Infrastructure.Mongo.Documents +{ + public static class Extensions + { + public static FileSourceInfoDocument AsDocument(this FileSourceInfo fileSourceInfo) + => new FileSourceInfoDocument + { + Id = fileSourceInfo.Id, + SourceId = fileSourceInfo.SourceId, + SourceType = fileSourceInfo.SourceType, + UploaderId = fileSourceInfo.UploaderId, + State = fileSourceInfo.State, + CreatedAt = fileSourceInfo.CreatedAt, + OriginalFileId = fileSourceInfo.OriginalFileId, + OriginalFileContentType = fileSourceInfo.OriginalFileContentType, + FileId = fileSourceInfo.FileId, + FileName = fileSourceInfo.FileName + }; + + public static FileSourceInfo AsEntity(this FileSourceInfoDocument document) + => new FileSourceInfo(document.Id, document.SourceId, document.SourceType, document.UploaderId, + document.State, document.CreatedAt, document.OriginalFileId, document.OriginalFileContentType, + document.FileId, document.FileName); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Mongo/Documents/FileSourceInfoDocument.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Mongo/Documents/FileSourceInfoDocument.cs new file mode 100644 index 00000000..8aba3c17 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Mongo/Documents/FileSourceInfoDocument.cs @@ -0,0 +1,20 @@ +using Convey.Types; +using MiniSpace.Services.MediaFiles.Core.Entities; +using MongoDB.Bson; + +namespace MiniSpace.Services.MediaFiles.Infrastructure.Mongo.Documents +{ + public class FileSourceInfoDocument : IIdentifiable + { + public Guid Id { get; set; } + public Guid SourceId { get; set; } + public ContextType SourceType { get; set; } + public Guid UploaderId { get; set; } + public State State { get; set; } + public DateTime CreatedAt { get; set; } + public ObjectId OriginalFileId { get; set; } + public string OriginalFileContentType { get; set; } + public ObjectId FileId { get; set; } + public string FileName { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Mongo/Queries/Handlers/GetMediaFileHandler.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Mongo/Queries/Handlers/GetMediaFileHandler.cs new file mode 100644 index 00000000..2d5abd75 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Mongo/Queries/Handlers/GetMediaFileHandler.cs @@ -0,0 +1,45 @@ +using Convey.CQRS.Queries; +using Convey.Persistence.MongoDB; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Net.Http.Headers; +using MiniSpace.Services.MediaFiles.Application.Dto; +using MiniSpace.Services.MediaFiles.Application.Queries; +using MiniSpace.Services.MediaFiles.Application.Services; +using MiniSpace.Services.MediaFiles.Infrastructure.Mongo.Documents; +using MongoDB.Bson; + +namespace MiniSpace.Services.MediaFiles.Infrastructure.Mongo.Queries.Handlers +{ + public class GetMediaFileHandler : IQueryHandler + { + private readonly IMongoRepository _fileSourceInfoRepository; + private readonly IGridFSService _gridFSService; + private const string FileContentType = "image/webp"; + + public GetMediaFileHandler(IMongoRepository fileSourceInfoRepository, + IGridFSService gridFSService) + { + _fileSourceInfoRepository = fileSourceInfoRepository; + _gridFSService = gridFSService; + } + + public async Task HandleAsync(GetMediaFile query, CancellationToken cancellationToken) + { + var fileSourceInfo = await _fileSourceInfoRepository.GetAsync(query.MediaFileId); + if (fileSourceInfo is null) + { + return null; + } + + var fileStream = new MemoryStream(); + await _gridFSService.DownloadFileAsync(fileSourceInfo.FileId, fileStream); + fileStream.Seek(0, SeekOrigin.Begin); + byte[] fileContent = fileStream.ToArray(); + var base64String = Convert.ToBase64String(fileContent); + + return new FileDto(query.MediaFileId, fileSourceInfo.SourceId, fileSourceInfo.SourceType.ToString(), + fileSourceInfo.UploaderId, fileSourceInfo.State.ToString().ToLower(), fileSourceInfo.CreatedAt, + fileSourceInfo.FileName, FileContentType, base64String); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Mongo/Queries/Handlers/GetOriginalMediaFileHandler.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Mongo/Queries/Handlers/GetOriginalMediaFileHandler.cs new file mode 100644 index 00000000..3e8edf82 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Mongo/Queries/Handlers/GetOriginalMediaFileHandler.cs @@ -0,0 +1,44 @@ +using Convey.CQRS.Queries; +using Convey.Persistence.MongoDB; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Net.Http.Headers; +using MiniSpace.Services.MediaFiles.Application.Dto; +using MiniSpace.Services.MediaFiles.Application.Queries; +using MiniSpace.Services.MediaFiles.Application.Services; +using MiniSpace.Services.MediaFiles.Infrastructure.Mongo.Documents; +using MongoDB.Bson; + +namespace MiniSpace.Services.MediaFiles.Infrastructure.Mongo.Queries.Handlers +{ + public class GetOriginalMediaFileHandler : IQueryHandler + { + private readonly IMongoRepository _fileSourceInfoRepository; + private readonly IGridFSService _gridFSService; + + public GetOriginalMediaFileHandler(IMongoRepository fileSourceInfoRepository, + IGridFSService gridFSService) + { + _fileSourceInfoRepository = fileSourceInfoRepository; + _gridFSService = gridFSService; + } + + public async Task HandleAsync(GetOriginalMediaFile query, CancellationToken cancellationToken) + { + var fileSourceInfo = await _fileSourceInfoRepository.GetAsync(query.MediaFileId); + if (fileSourceInfo is null) + { + return null; + } + + var fileStream = new MemoryStream(); + await _gridFSService.DownloadFileAsync(fileSourceInfo.OriginalFileId, fileStream); + fileStream.Seek(0, SeekOrigin.Begin); + byte[] fileContent = fileStream.ToArray(); + var base64String = Convert.ToBase64String(fileContent); + + return new FileDto(query.MediaFileId, fileSourceInfo.SourceId, fileSourceInfo.SourceType.ToString(), + fileSourceInfo.UploaderId, fileSourceInfo.State.ToString().ToLower(), fileSourceInfo.CreatedAt, + fileSourceInfo.FileName, fileSourceInfo.OriginalFileContentType, base64String); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Mongo/Repositories/FileSourceInfoMongoRepository.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Mongo/Repositories/FileSourceInfoMongoRepository.cs new file mode 100644 index 00000000..c3e80cbc --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Mongo/Repositories/FileSourceInfoMongoRepository.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Convey.Persistence.MongoDB; +using MiniSpace.Services.MediaFiles.Core.Entities; +using MiniSpace.Services.MediaFiles.Core.Repositories; +using MiniSpace.Services.MediaFiles.Infrastructure.Mongo.Documents; +using MongoDB.Driver; +using MongoDB.Driver.Linq; + +namespace MiniSpace.Services.MediaFiles.Infrastructure.Mongo.Repositories +{ + public class FileSourceInfoMongoRepository : IFileSourceInfoRepository + { + private readonly IMongoRepository _repository; + + public FileSourceInfoMongoRepository(IMongoRepository repository) + { + _repository = repository; + } + + public async Task GetAsync(Guid id) + { + var fileSourceInfo = await _repository.GetAsync(s => s.Id == id); + + return fileSourceInfo?.AsEntity(); + } + + public async Task> GetAllUnassociatedAsync() + { + var fileSourceInfos = await _repository.FindAsync(s => s.State == State.Unassociated); + + return fileSourceInfos?.Select(s => s.AsEntity()); + } + + public Task AddAsync(FileSourceInfo fileSourceInfo) + => _repository.AddAsync(fileSourceInfo.AsDocument()); + + public Task UpdateAsync(FileSourceInfo fileSourceInfo) + => _repository.UpdateAsync(fileSourceInfo.AsDocument()); + + public Task DeleteAsync(Guid id) + => _repository.DeleteAsync(id); + + public Task ExistsAsync(Guid id) + => _repository.ExistsAsync(s => s.Id == id); + + public async Task> FindAsync(Guid sourceId, ContextType sourceType) + { + var fileSourceInfos = await _repository.FindAsync( + s => s.SourceId == sourceId && s.SourceType == sourceType); + + return fileSourceInfos?.Select(s => s.AsEntity()); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/DateTimeProvider.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/DateTimeProvider.cs new file mode 100644 index 00000000..539d6089 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/DateTimeProvider.cs @@ -0,0 +1,9 @@ +using MiniSpace.Services.MediaFiles.Application.Services; + +namespace MiniSpace.Services.MediaFiles.Infrastructure.Services +{ + internal sealed class DateTimeProvider : IDateTimeProvider + { + public DateTime Now => DateTime.UtcNow; + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/EventMapper.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/EventMapper.cs new file mode 100644 index 00000000..ff2fb0c3 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/EventMapper.cs @@ -0,0 +1,23 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.MediaFiles.Application.Services; +using MiniSpace.Services.MediaFiles.Core; +using MiniSpace.Services.MediaFiles.Core.Events; + +namespace MiniSpace.Services.MediaFiles.Infrastructure.Services +{ + public class EventMapper : IEventMapper + { + public IEnumerable MapAll(IEnumerable events) + => events.Select(Map); + + public IEvent Map(IDomainEvent @event) + { + switch (@event) + { + + } + + return null; + } + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/FileValidator.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/FileValidator.cs new file mode 100644 index 00000000..30cf2b34 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/FileValidator.cs @@ -0,0 +1,49 @@ +using MiniSpace.Services.MediaFiles.Application.Exceptions; +using MiniSpace.Services.MediaFiles.Application.Services; + +namespace MiniSpace.Services.MediaFiles.Infrastructure.Services +{ + public class FileValidator : IFileValidator + { + private const int MaxFileSize = 1_000_000; + + private readonly Dictionary _mimeTypes = new Dictionary() + { + { "FFD8FFDB", "image/jpeg" }, + { "FFD8FFE0", "image/jpeg" }, + { "FFD8FFE1", "image/jpeg" }, + { "FFD8FFE2", "image/jpeg" }, + { "FFD8FFEE", "image/jpeg" }, + { "89504E47", "image/png" }, + { "47494638", "image/gif" }, + { "49492A00", "image/tiff" }, + { "4D4D002A", "image/tiff" }, + { "52494646", "image/webp" }, + { "57454250", "image/webp" } + }; + + public void ValidateFileSize(int size) + { + if (size > MaxFileSize) + { + throw new InvalidFileSizeException(size, MaxFileSize); + } + } + + public void ValidateFileExtensions(byte[] bytes, string contentType) + { + if (!_mimeTypes.ContainsValue(contentType)) + { + throw new InvalidFileContentTypeException(contentType); + } + + string hex = BitConverter.ToString(bytes, 0, 4).Replace("-", string.Empty); + _mimeTypes.TryGetValue(hex, out var mimeType); + if (mimeType != contentType) + { + throw new FileTypeDoesNotMatchContentTypeException(mimeType, contentType); + } + } + + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/GridFSService.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/GridFSService.cs new file mode 100644 index 00000000..35c7e60a --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/GridFSService.cs @@ -0,0 +1,35 @@ +using MiniSpace.Services.MediaFiles.Application.Services; +using MongoDB.Bson; +using MongoDB.Driver; +using MongoDB.Driver.GridFS; + +namespace MiniSpace.Services.MediaFiles.Infrastructure.Services +{ + public class GridFSService: IGridFSService + { + private readonly IMongoDatabase _database; + private readonly GridFSBucket _gridFSBucket; + + public GridFSService(IMongoDatabase database) + { + _database = database; + _gridFSBucket = new GridFSBucket(_database); + } + + public async Task UploadFileAsync(string fileName, Stream fileStream) + { + ObjectId fileId = await _gridFSBucket.UploadFromStreamAsync(fileName, fileStream); + return fileId; + } + + public async Task DownloadFileAsync(ObjectId fileId, Stream destination) + { + await _gridFSBucket.DownloadToStreamAsync(fileId, destination); + } + + public async Task DeleteFileAsync(ObjectId fileId) + { + await _gridFSBucket.DeleteAsync(fileId); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/MediaFilesService.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/MediaFilesService.cs new file mode 100644 index 00000000..4d540f28 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/MediaFilesService.cs @@ -0,0 +1,71 @@ +using MiniSpace.Services.MediaFiles.Application; +using MiniSpace.Services.MediaFiles.Application.Commands; +using MiniSpace.Services.MediaFiles.Application.Dto; +using MiniSpace.Services.MediaFiles.Application.Events; +using MiniSpace.Services.MediaFiles.Application.Exceptions; +using MiniSpace.Services.MediaFiles.Application.Services; +using MiniSpace.Services.MediaFiles.Core.Entities; +using MiniSpace.Services.MediaFiles.Core.Repositories; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Webp; + +namespace MiniSpace.Services.MediaFiles.Infrastructure.Services +{ + public class MediaFilesService: IMediaFilesService + { + private readonly IFileSourceInfoRepository _fileSourceInfoRepository; + private readonly IFileValidator _fileValidator; + private readonly IGridFSService _gridFSService; + private readonly IDateTimeProvider _dateTimeProvider; + private readonly IAppContext _appContext; + private readonly IMessageBroker _messageBroker; + + public MediaFilesService(IFileSourceInfoRepository fileSourceInfoRepository, IFileValidator fileValidator, + IGridFSService gridFSService, IDateTimeProvider dateTimeProvider, IAppContext appContext, + IMessageBroker messageBroker) + { + _fileSourceInfoRepository = fileSourceInfoRepository; + _fileValidator = fileValidator; + _gridFSService = gridFSService; + _dateTimeProvider = dateTimeProvider; + _appContext = appContext; + _messageBroker = messageBroker; + } + + public async Task UploadAsync(UploadMediaFile command) + { + var identity = _appContext.Identity; + if(identity.IsAuthenticated && identity.Id != command.UploaderId) + { + throw new UnauthorizedMediaFileUploadException(identity.Id, command.UploaderId); + } + + if (!Enum.TryParse(command.SourceType, out ContextType sourceType)) + { + throw new InvalidContextTypeException(command.SourceType); + } + + byte[] bytes = Convert.FromBase64String(command.Base64Content); + _fileValidator.ValidateFileSize(bytes.Length); + _fileValidator.ValidateFileExtensions(bytes, command.FileContentType); + + using var inStream = new MemoryStream(bytes); + using var myImage = await Image.LoadAsync(inStream); + using var outStream = new MemoryStream(); + await myImage.SaveAsync(outStream, new WebpEncoder()); + inStream.Position = 0; + outStream.Position = 0; + + var originalObjectId = await _gridFSService.UploadFileAsync(command.FileName, inStream); + var objectId = await _gridFSService.UploadFileAsync(command.FileName, outStream); + var fileSourceInfo = new FileSourceInfo(command.MediaFileId, command.SourceId, sourceType, + command.UploaderId, State.Unassociated, _dateTimeProvider.Now, originalObjectId, + command.FileContentType, objectId, command.FileName); + await _fileSourceInfoRepository.AddAsync(fileSourceInfo); + await _messageBroker.PublishAsync(new MediaFileUploaded(command.MediaFileId, command.FileName)); + + return new FileUploadResponseDto(fileSourceInfo.Id); + } + + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/MessageBroker.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/MessageBroker.cs new file mode 100644 index 00000000..5c35834e --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/MessageBroker.cs @@ -0,0 +1,84 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; +using Convey.MessageBrokers.Outbox; +using Convey.MessageBrokers.RabbitMQ; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using OpenTracing; +using MiniSpace.Services.MediaFiles.Application.Services; + +namespace MiniSpace.Services.MediaFiles.Infrastructure.Services +{ + internal sealed class MessageBroker : IMessageBroker + { + private const string DefaultSpanContextHeader = "span_context"; + private readonly IBusPublisher _busPublisher; + private readonly IMessageOutbox _outbox; + private readonly ICorrelationContextAccessor _contextAccessor; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IMessagePropertiesAccessor _messagePropertiesAccessor; + private readonly ITracer _tracer; + private readonly ILogger _logger; + private readonly string _spanContextHeader; + + public MessageBroker(IBusPublisher busPublisher, IMessageOutbox outbox, + ICorrelationContextAccessor contextAccessor, IHttpContextAccessor httpContextAccessor, + IMessagePropertiesAccessor messagePropertiesAccessor, RabbitMqOptions options, ITracer tracer, + ILogger logger) + { + _busPublisher = busPublisher; + _outbox = outbox; + _contextAccessor = contextAccessor; + _httpContextAccessor = httpContextAccessor; + _messagePropertiesAccessor = messagePropertiesAccessor; + _tracer = tracer; + _logger = logger; + _spanContextHeader = string.IsNullOrWhiteSpace(options.SpanContextHeader) + ? DefaultSpanContextHeader + : options.SpanContextHeader; + } + + public Task PublishAsync(params IEvent[] events) => PublishAsync(events?.AsEnumerable()); + + public async Task PublishAsync(IEnumerable events) + { + if (events is null) + { + return; + } + + var messageProperties = _messagePropertiesAccessor.MessageProperties; + var originatedMessageId = messageProperties?.MessageId; + var correlationId = messageProperties?.CorrelationId; + var spanContext = messageProperties?.GetSpanContext(_spanContextHeader); + if (string.IsNullOrWhiteSpace(spanContext)) + { + spanContext = _tracer.ActiveSpan is null ? string.Empty : _tracer.ActiveSpan.Context.ToString(); + } + + var headers = messageProperties.GetHeadersToForward(); + var correlationContext = _contextAccessor.CorrelationContext ?? + _httpContextAccessor.GetCorrelationContext(); + + foreach (var @event in events) + { + if (@event is null) + { + continue; + } + + var messageId = Guid.NewGuid().ToString("N"); + _logger.LogTrace($"Publishing integration event: {@event.GetType().Name} [id: '{messageId}']."); + if (_outbox.Enabled) + { + await _outbox.SendAsync(@event, originatedMessageId, messageId, correlationId, spanContext, + correlationContext, headers); + continue; + } + + await _busPublisher.PublishAsync(@event, messageId, correlationId, spanContext, correlationContext, + headers); + } + } + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/Workers/FileCleanupWorker.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/Workers/FileCleanupWorker.cs new file mode 100644 index 00000000..243d9da1 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/Workers/FileCleanupWorker.cs @@ -0,0 +1,57 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Commands; +using Convey.Persistence.MongoDB; +using Microsoft.Extensions.Hosting; +using MiniSpace.Services.MediaFiles.Application.Commands; +using MiniSpace.Services.MediaFiles.Application.Events; +using MiniSpace.Services.MediaFiles.Application.Services; +using MiniSpace.Services.MediaFiles.Infrastructure.Mongo.Documents; + +namespace MiniSpace.Services.MediaFiles.Infrastructure.Services.Workers +{ + public class FileCleanupWorker: BackgroundService + { + private readonly IMessageBroker _messageBroker; + private readonly ICommandDispatcher _commandDispatcher; + private readonly IDateTimeProvider _dateTimeProvider; + private const int MinutesInterval = 10; + + public FileCleanupWorker(IMessageBroker messageBroker, ICommandDispatcher commandDispatcher, + IDateTimeProvider dateTimeProvider) + { + _messageBroker = messageBroker; + _commandDispatcher = commandDispatcher; + _dateTimeProvider = dateTimeProvider; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + await _messageBroker.PublishAsync(new FileCleanupBackgroundWorkerStarted(_dateTimeProvider.Now)); + while (!stoppingToken.IsCancellationRequested) + { + try + { + var now = _dateTimeProvider.Now; + var minutes = now.Minute; + if (minutes % MinutesInterval == 0) + { + await _commandDispatcher.SendAsync(new CleanupUnassociatedFiles(now), stoppingToken); + } + + var nextTime = now.AddMinutes(MinutesInterval - (minutes % MinutesInterval)).AddSeconds(-now.Second) + .AddMilliseconds(-now.Millisecond); + var delay = nextTime - now; + + await Task.Delay(delay, stoppingToken); + } + catch (TaskCanceledException) + { + await _messageBroker.PublishAsync(new FileCleanupBackgroundWorkerStopped(_dateTimeProvider.Now)); + return; + } + } + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Notifications/.gitignore b/MiniSpace.Services.Notifications/.gitignore new file mode 100644 index 00000000..64def56a --- /dev/null +++ b/MiniSpace.Services.Notifications/.gitignore @@ -0,0 +1,331 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ +# **/Properties/launchSettings.json + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +logs/ diff --git a/MiniSpace.Services.Notifications/Dockerfile b/MiniSpace.Services.Notifications/Dockerfile new file mode 100644 index 00000000..a75e7b0d --- /dev/null +++ b/MiniSpace.Services.Notifications/Dockerfile @@ -0,0 +1,17 @@ +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /app + +COPY . . + +RUN dotnet publish src/MiniSpace.Services.Notifications.Api -c Release -o out + +FROM mcr.microsoft.com/dotnet/aspnet:8.0 +WORKDIR /app + +COPY --from=build /app/out . + +ENV ASPNETCORE_URLS=http://*:80 +ENV ASPNETCORE_ENVIRONMENT=docker +ENV NTRADA_CONFIG=ntrada.docker + +ENTRYPOINT ["dotnet", "MiniSpace.Services.Notifications.Api.dll"] \ No newline at end of file diff --git a/MiniSpace.Services.Notifications/LICENSE b/MiniSpace.Services.Notifications/LICENSE new file mode 100644 index 00000000..b7ea7f0c --- /dev/null +++ b/MiniSpace.Services.Notifications/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 DevMentors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MiniSpace.Services.Notifications/MiniSpace.Services.Notifications.sln b/MiniSpace.Services.Notifications/MiniSpace.Services.Notifications.sln new file mode 100644 index 00000000..ff5faab9 --- /dev/null +++ b/MiniSpace.Services.Notifications/MiniSpace.Services.Notifications.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{0E976732-0CD9-4D2E-B989-998B124073BA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.Students.Api", "src\MiniSpace.Services.Students.Api\MiniSpace.Services.Students.Api.csproj", "{D915BFB5-D2D4-44C8-A3A3-379419079F06}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.Students.Application", "src\MiniSpace.Services.Students.Application\MiniSpace.Services.Students.Application.csproj", "{97B39658-9B33-4124-90E5-102FDA7D3733}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.Students.Core", "src\MiniSpace.Services.Students.Core\MiniSpace.Services.Students.Core.csproj", "{C4EFD7FD-9D95-444B-86A4-E4D60E62CD16}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.Students.Infrastructure", "src\MiniSpace.Services.Students.Infrastructure\MiniSpace.Services.Students.Infrastructure.csproj", "{B2997BE8-0CE3-45DD-98E9-80599B070C25}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D915BFB5-D2D4-44C8-A3A3-379419079F06}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D915BFB5-D2D4-44C8-A3A3-379419079F06}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D915BFB5-D2D4-44C8-A3A3-379419079F06}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D915BFB5-D2D4-44C8-A3A3-379419079F06}.Release|Any CPU.Build.0 = Release|Any CPU + {97B39658-9B33-4124-90E5-102FDA7D3733}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {97B39658-9B33-4124-90E5-102FDA7D3733}.Debug|Any CPU.Build.0 = Debug|Any CPU + {97B39658-9B33-4124-90E5-102FDA7D3733}.Release|Any CPU.ActiveCfg = Release|Any CPU + {97B39658-9B33-4124-90E5-102FDA7D3733}.Release|Any CPU.Build.0 = Release|Any CPU + {C4EFD7FD-9D95-444B-86A4-E4D60E62CD16}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C4EFD7FD-9D95-444B-86A4-E4D60E62CD16}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C4EFD7FD-9D95-444B-86A4-E4D60E62CD16}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C4EFD7FD-9D95-444B-86A4-E4D60E62CD16}.Release|Any CPU.Build.0 = Release|Any CPU + {B2997BE8-0CE3-45DD-98E9-80599B070C25}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2997BE8-0CE3-45DD-98E9-80599B070C25}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2997BE8-0CE3-45DD-98E9-80599B070C25}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2997BE8-0CE3-45DD-98E9-80599B070C25}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {D915BFB5-D2D4-44C8-A3A3-379419079F06} = {0E976732-0CD9-4D2E-B989-998B124073BA} + {97B39658-9B33-4124-90E5-102FDA7D3733} = {0E976732-0CD9-4D2E-B989-998B124073BA} + {C4EFD7FD-9D95-444B-86A4-E4D60E62CD16} = {0E976732-0CD9-4D2E-B989-998B124073BA} + {B2997BE8-0CE3-45DD-98E9-80599B070C25} = {0E976732-0CD9-4D2E-B989-998B124073BA} + EndGlobalSection +EndGlobal diff --git a/MiniSpace.Services.Notifications/scripts/build.sh b/MiniSpace.Services.Notifications/scripts/build.sh new file mode 100644 index 00000000..3affad0e --- /dev/null +++ b/MiniSpace.Services.Notifications/scripts/build.sh @@ -0,0 +1,2 @@ +#!/bin/bash +dotnet build -c release \ No newline at end of file diff --git a/MiniSpace.Services.Notifications/scripts/dockerize-tag-push.sh b/MiniSpace.Services.Notifications/scripts/dockerize-tag-push.sh new file mode 100755 index 00000000..e3a7da2a --- /dev/null +++ b/MiniSpace.Services.Notifications/scripts/dockerize-tag-push.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +export ASPNETCORE_ENVIRONMENT=docker + +cd .. + +docker build -t minispace.services.notifications:latest . + +docker tag minispace.services.notifications:latest adrianvsaint/minispace.services.notifications:latest + +docker push adrianvsaint/minispace.services.notifications:latest diff --git a/MiniSpace.Services.Notifications/scripts/start.sh b/MiniSpace.Services.Notifications/scripts/start.sh new file mode 100644 index 00000000..28a92d91 --- /dev/null +++ b/MiniSpace.Services.Notifications/scripts/start.sh @@ -0,0 +1,4 @@ +#!/bin/bash +export ASPNETCORE_ENVIRONMENT=local +cd ../src/MiniSpace.Services.Notifications.Api +dotnet run \ No newline at end of file diff --git a/MiniSpace.Services.Notifications/scripts/test.sh b/MiniSpace.Services.Notifications/scripts/test.sh new file mode 100644 index 00000000..6046c35a --- /dev/null +++ b/MiniSpace.Services.Notifications/scripts/test.sh @@ -0,0 +1,2 @@ +#!/bin/bash +dotnet test \ No newline at end of file diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Api/MiniSpace.Services.Notifications.Api.csproj b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Api/MiniSpace.Services.Notifications.Api.csproj new file mode 100644 index 00000000..16804845 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Api/MiniSpace.Services.Notifications.Api.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + latest + MiniSpace.Services.Students.Api + + + + + + + + + + + + + + diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Api/MiniSpace.Services.Notifications.Api.sln b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Api/MiniSpace.Services.Notifications.Api.sln new file mode 100644 index 00000000..e8f3180b --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Api/MiniSpace.Services.Notifications.Api.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniSpace.Services.Notifications.Api", "MiniSpace.Services.Notifications.Api.csproj", "{5DA6DF02-FFC5-4BAB-B1FC-739A9DA14602}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {5DA6DF02-FFC5-4BAB-B1FC-739A9DA14602}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5DA6DF02-FFC5-4BAB-B1FC-739A9DA14602}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5DA6DF02-FFC5-4BAB-B1FC-739A9DA14602}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5DA6DF02-FFC5-4BAB-B1FC-739A9DA14602}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {284B46A7-2ABB-4E20-8B52-4291CC46AE00} + EndGlobalSection +EndGlobal diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Api/Program.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Api/Program.cs new file mode 100644 index 00000000..de2217e7 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Api/Program.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Convey; +using Convey.Logging; +using Convey.Types; +using Convey.WebApi; +using Convey.WebApi.CQRS; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using MiniSpace.Services.Notifications.Application; +using MiniSpace.Services.Notifications.Application.Commands; +using MiniSpace.Services.Notifications.Application.Dto; +using MiniSpace.Services.Notifications.Application.Queries; +using MiniSpace.Services.Notifications.Infrastructure; + +namespace MiniSpace.Services.Notifications.Api +{ + public class Program + { + public static async Task Main(string[] args) + => await WebHost.CreateDefaultBuilder(args) + .ConfigureServices(services => services + .AddConvey() + .AddWebApi() + .AddApplication() + .AddInfrastructure() + .Build()) + .Configure(app => app + .UseInfrastructure() + .UseDispatcherEndpoints(endpoints => endpoints + .Get("", ctx => ctx.Response.WriteAsync(ctx.RequestServices.GetService().Name)) + .Get>("notifications/{userId}") + .Get("notifications/{userId}/{notificationId}") + .Post("notifications") + .Put("notifications/{userId}/{notificationId}/status") + .Delete("notifications/notification/{userId}/{notificationId}"))) + .UseLogging() + .UseLogging() + .Build() + .RunAsync(); + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Api/Properties/launchSettings.json b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Api/Properties/launchSettings.json new file mode 100644 index 00000000..43876a98 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Api/Properties/launchSettings.json @@ -0,0 +1,26 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:5006" + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": false, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "local" + } + }, + "MiniSpace.Services.Students": { + "commandName": "Project", + "launchBrowser": false, + "applicationUrl": "http://localhost:5006", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "local" + } + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Api/appsettings.Development.json b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Api/appsettings.Development.json new file mode 100644 index 00000000..2c63c085 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Api/appsettings.Development.json @@ -0,0 +1,2 @@ +{ +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Api/appsettings.docker.json b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Api/appsettings.docker.json new file mode 100644 index 00000000..2e098d3d --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Api/appsettings.docker.json @@ -0,0 +1,158 @@ +{ + "app": { + "name": "MiniSpace Notifications Service", + "version": "1" + }, + "consul": { + "enabled": true, + "url": "http://consul:8500", + "service": "notifications-service", + "address": "notifications-service", + "port": "80", + "pingEnabled": true, + "pingEndpoint": "ping", + "pingInterval": 3, + "removeAfterInterval": 3 + }, + "fabio": { + "enabled": true, + "url": "http://fabio:9999", + "service": "students-service" + }, + "httpClient": { + "type": "fabio", + "retries": 3, + "services": { + "events": "events-service", + "posts": "posts-service", + "friends": "friends-service", + "students": "students-service" + } + }, + "jwt": { + "certificate": { + "location": "", + "password": "", + "rawData": "" + }, + "issuerSigningKey": "eiquief5phee9pazo0Faegaez9gohThailiur5woy2befiech1oarai4aiLi6ahVecah3ie9Aiz6Peij", + "expiryMinutes": 60, + "issuer": "minispace", + "validateAudience": false, + "validateIssuer": false, + "validateLifetime": true, + "allowAnonymousEndpoints": ["/sign-in", "/sign-up"] + }, + "logger": { + "console": { + "enabled": true + }, + "elk": { + "enabled": false, + "url": "http://elk:9200" + }, + "file": { + "enabled": false, + "path": "logs/logs.txt", + "interval": "day" + }, + "seq": { + "enabled": true, + "url": "http://seq:5341", + "apiKey": "secret" + } + }, + "jaeger": { + "enabled": true, + "serviceName": "notifications", + "udpHost": "jaeger", + "udpPort": 6831, + "maxPacketSize": 0, + "sampler": "const", + "excludePaths": ["/", "/ping", "/metrics"] + }, + "metrics": { + "enabled": true, + "influxEnabled": false, + "prometheusEnabled": true, + "influxUrl": "http://influx:8086", + "database": "minispace", + "env": "docker", + "interval": 5 + }, + "mongo": { + "connectionString": "mongodb+srv://minispace-user:9vd6IxYWUuuqhzEH@cluster0.mmhq4pe.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0", + "database": "notifications-service", + "seed": false + }, + "rabbitMq": { + "connectionName": "notifications-service", + "retries": 3, + "retryInterval": 2, + "conventionsCasing": "snakeCase", + "logger": { + "enabled": true + }, + "username": "guest", + "password": "guest", + "virtualHost": "/", + "port": 5672, + "hostnames": [ + "rabbitmq" + ], + "requestedConnectionTimeout": "00:00:30", + "requestedHeartbeat": "00:01:00", + "socketReadTimeout": "00:00:30", + "socketWriteTimeout": "00:00:30", + "continuationTimeout": "00:00:20", + "handshakeContinuationTimeout": "00:00:10", + "networkRecoveryInterval": "00:00:05", + "exchange": { + "declare": true, + "durable": true, + "autoDelete": false, + "type": "topic", + "name": "notifications" + }, + "queue": { + "declare": true, + "durable": true, + "exclusive": false, + "autoDelete": false, + "template": "notifications-service/{{exchange}}.{{message}}" + }, + "context": { + "enabled": true, + "header": "message_context" + }, + "spanContextHeader": "span_context" + }, + "redis": { + "connectionString": "redis", + "instance": "notifications:" + }, + "swagger": { + "enabled": true, + "reDocEnabled": false, + "name": "v1", + "title": "API", + "version": "v1", + "routePrefix": "docs", + "includeSecurity": true + }, + "vault": { + "enabled": false, + "url": "http://vault:8200", + "kv": { + "enabled": false + }, + "pki": { + "enabled": false + }, + "lease": { + "mongo": { + "enabled": false + } + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Api/appsettings.json b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Api/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Api/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Api/appsettings.local.json b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Api/appsettings.local.json new file mode 100644 index 00000000..14044b3d --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Api/appsettings.local.json @@ -0,0 +1,206 @@ +{ + "app": { + "name": "MiniSpace Notifications Service", + "service": "notifications-service", + "version": "1" + }, + "consul": { + "enabled": true, + "url": "http://localhost:8500", + "service": "notifications-service", + "address": "docker.for.win.localhost", + "port": "5007", + "pingEnabled": false, + "pingEndpoint": "ping", + "pingInterval": 3, + "removeAfterInterval": 3 + }, + "fabio": { + "enabled": true, + "url": "http://localhost:9999", + "service": "notifications-service" + }, + "httpClient": { + "type": "direct", + "retries": 3, + "services": { + "events": "http://localhost:5008", + "posts": "http://localhost:5013", + "comments": "http://localhost:5009", + "friends": "http://localhost:5012", + "organizations": "http://localhost:5015", + "students": "http://localhost:5007" + }, + "requestMasking": { + "enabled": true, + "maskTemplate": "*****" + } + }, + "jwt": { + "certificate": { + "location": "certs/localhost.pfx", + "password": "test", + "rawData": "" + }, + "issuerSigningKey": "eiquief5phee9pazo0Faegaez9gohThailiur5woy2befiech1oarai4aiLi6ahVecah3ie9Aiz6Peij", + "expiryMinutes": 60, + "issuer": "minispace", + "validateAudience": false, + "validateIssuer": false, + "validateLifetime": false, + "allowAnonymousEndpoints": ["/sign-in", "/sign-up"] + }, + "logger": { + "level": "information", + "excludePaths": ["/", "/ping", "/metrics"], + "excludeProperties": [ + "api_key", + "access_key", + "ApiKey", + "ApiSecret", + "ClientId", + "ClientSecret", + "ConnectionString", + "Password", + "Email", + "Login", + "Secret", + "Token" + ], + "console": { + "enabled": true + }, + "elk": { + "enabled": false, + "url": "http://localhost:9200" + }, + "file": { + "enabled": true, + "path": "logs/logs.txt", + "interval": "day" + }, + "seq": { + "enabled": true, + "url": "http://localhost:5341", + "apiKey": "secret" + }, + "tags": {} + }, + "jaeger": { + "enabled": true, + "serviceName": "notifications", + "udpHost": "localhost", + "udpPort": 6831, + "maxPacketSize": 0, + "sampler": "const", + "excludePaths": ["/", "/ping", "/metrics"] + }, + "metrics": { + "enabled": true, + "influxEnabled": false, + "prometheusEnabled": true, + "influxUrl": "http://localhost:8086", + "database": "minispace", + "env": "local", + "interval": 5 + }, + "mongo": { + "connectionString": "mongodb+srv://minispace-user:9vd6IxYWUuuqhzEH@cluster0.mmhq4pe.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0", + "database": "notifications-service", + "seed": false + }, + "outbox": { + "enabled": true, + "type": "sequential", + "expiry": 3600, + "intervalMilliseconds": 2000, + "inboxCollection": "inbox", + "outboxCollection": "outbox", + "disableTransactions": true + }, + "rabbitMq": { + "connectionName": "notifications-service", + "retries": 3, + "retryInterval": 2, + "conventionsCasing": "snakeCase", + "logger": { + "enabled": true + }, + "username": "guest", + "password": "guest", + "virtualHost": "/", + "port": 5672, + "hostnames": [ + "localhost" + ], + "requestedConnectionTimeout": "00:00:30", + "requestedHeartbeat": "00:01:00", + "socketReadTimeout": "00:00:30", + "socketWriteTimeout": "00:00:30", + "continuationTimeout": "00:00:20", + "handshakeContinuationTimeout": "00:00:10", + "networkRecoveryInterval": "00:00:05", + "exchange": { + "declare": true, + "durable": true, + "autoDelete": false, + "type": "topic", + "name": "notifications" + }, + "queue": { + "declare": true, + "durable": true, + "exclusive": false, + "autoDelete": false, + "template": "notifications-service/{{exchange}}.{{message}}" + }, + "context": { + "enabled": true, + "header": "message_context" + }, + "spanContextHeader": "span_context" + }, + "redis": { + "connectionString": "localhost", + "instance": "notifications:" + }, + "swagger": { + "enabled": true, + "reDocEnabled": false, + "name": "v1", + "title": "API", + "version": "v1", + "routePrefix": "docs", + "includeSecurity": true + }, + "vault": { + "enabled": true, + "url": "http://localhost:8200", + "authType": "token", + "token": "secret", + "username": "user", + "password": "secret", + "kv": { + "enabled": true, + "engineVersion": 2, + "mountPoint": "kv", + "path": "notifications-service/settings" + }, + "pki": { + "enabled": true, + "roleName": "notifications-service", + "commonName": "notifications-service.minispace.io" + }, + "lease": { + "mongo": { + "type": "database", + "roleName": "notifications-service", + "enabled": true, + "autoRenewal": true, + "templates": { + "connectionString": "mongodb://{{username}}:{{password}}@localhost:27017" + } + } + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Commands/CreateNotification.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Commands/CreateNotification.cs new file mode 100644 index 00000000..ecf53a12 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Commands/CreateNotification.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Notifications.Application.Commands +{ + public class CreateNotification : ICommand + { + public Guid NotificationId { get; } + public Guid UserId { get; } + public string Message { get; } + + public CreateNotification(Guid notificationId, Guid userId, string message) + { + NotificationId = notificationId; + UserId = userId; + Message = message; + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Commands/DeleteNotification.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Commands/DeleteNotification.cs new file mode 100644 index 00000000..f16b858b --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Commands/DeleteNotification.cs @@ -0,0 +1,20 @@ +using Convey.CQRS.Commands; +using Microsoft.AspNetCore.Mvc; + +namespace MiniSpace.Services.Notifications.Application.Commands +{ + public class DeleteNotification : ICommand + { + [FromQuery] + public Guid UserId { get; } + + [FromRoute] + public Guid NotificationId { get; } + + public DeleteNotification(Guid userId, Guid notificationId) + { + UserId = userId; + NotificationId = notificationId; + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Commands/Handlers/CreateNotificationHandler.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Commands/Handlers/CreateNotificationHandler.cs new file mode 100644 index 00000000..84ed463b --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Commands/Handlers/CreateNotificationHandler.cs @@ -0,0 +1,40 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Notifications.Core.Repositories; +using MiniSpace.Services.Notifications.Core.Entities; +using MiniSpace.Services.Notifications.Application.Services; + +namespace MiniSpace.Services.Notifications.Application.Commands.Handlers +{ + public class CreateNotificationHandler : ICommandHandler + { + private readonly INotificationRepository _notificationRepository; + private readonly IEventMapper _eventMapper; + private readonly IMessageBroker _messageBroker; + + public CreateNotificationHandler(INotificationRepository notificationRepository, IEventMapper eventMapper, IMessageBroker messageBroker) + { + _notificationRepository = notificationRepository; + _eventMapper = eventMapper; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(CreateNotification command, CancellationToken cancellationToken = default) + { + var notification = new Notification( + command.NotificationId, + command.UserId, + command.Message, + NotificationStatus.Unread, + DateTime.UtcNow, + null, + NotificationEventType.Other, + command.UserId + + ); + await _notificationRepository.AddAsync(notification); + + var events = _eventMapper.MapAll(notification.Events); + await _messageBroker.PublishAsync(events.ToArray()); + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Commands/Handlers/DeleteNotificationHandler.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Commands/Handlers/DeleteNotificationHandler.cs new file mode 100644 index 00000000..051b777e --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Commands/Handlers/DeleteNotificationHandler.cs @@ -0,0 +1,38 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Notifications.Core.Repositories; +using MiniSpace.Services.Notifications.Application.Exceptions; +using MiniSpace.Services.Notifications.Application.Services; +using System.Threading.Tasks; +using System; +using MiniSpace.Services.Notifications.Application.Events.External; + +namespace MiniSpace.Services.Notifications.Application.Commands.Handlers +{ + public class DeleteNotificationHandler : ICommandHandler + { + private readonly IStudentNotificationsRepository _studentNotificationsRepository; + private readonly IEventMapper _eventMapper; + private readonly IMessageBroker _messageBroker; + + public DeleteNotificationHandler(IStudentNotificationsRepository notificationRepository, IEventMapper eventMapper, IMessageBroker messageBroker) + { + _studentNotificationsRepository = notificationRepository; + _eventMapper = eventMapper; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(DeleteNotification command, CancellationToken cancellationToken = default) + { + var exists = await _studentNotificationsRepository.NotificationExists(command.UserId, command.NotificationId); + if (!exists) + { + throw new NotificationNotFoundException(command.NotificationId); + } + + await _studentNotificationsRepository.DeleteNotification(command.UserId, command.NotificationId); + + var notificationDeletedEvent = new NotificationDeleted(command.UserId, command.NotificationId); + await _messageBroker.PublishAsync(notificationDeletedEvent); + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Commands/Handlers/UpdateNotificationStatusHandler.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Commands/Handlers/UpdateNotificationStatusHandler.cs new file mode 100644 index 00000000..ee4cb410 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Commands/Handlers/UpdateNotificationStatusHandler.cs @@ -0,0 +1,34 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Notifications.Core.Repositories; +using MiniSpace.Services.Notifications.Application.Exceptions; +using MiniSpace.Services.Notifications.Core.Entities; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Notifications.Application.Commands.Handlers { + public class UpdateNotificationStatusHandler : ICommandHandler + { + private readonly IStudentNotificationsRepository _studentNotificationsRepository; + + public UpdateNotificationStatusHandler(IStudentNotificationsRepository studentNotificationsRepository) + { + _studentNotificationsRepository = studentNotificationsRepository; + } + + public async Task HandleAsync(UpdateNotificationStatus command, CancellationToken cancellationToken = default) + { + var notificationExists = await _studentNotificationsRepository.NotificationExists(command.UserId, command.NotificationId); + if (!notificationExists) + throw new NotificationNotFoundException(command.NotificationId); + + if (Enum.TryParse(command.Status, true, out var newStatus)) + { + await _studentNotificationsRepository.UpdateNotificationStatus(command.UserId, command.NotificationId, newStatus.ToString()); + } + else + throw new ArgumentException($"Invalid status value: {command.Status}"); + } + + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Commands/UpdateNotificationStatus.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Commands/UpdateNotificationStatus.cs new file mode 100644 index 00000000..0677bf3b --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Commands/UpdateNotificationStatus.cs @@ -0,0 +1,24 @@ +using Convey.CQRS.Commands; +using Microsoft.AspNetCore.Mvc; + +namespace MiniSpace.Services.Notifications.Application.Commands +{ + public class UpdateNotificationStatus : ICommand + { + [FromRoute] + public Guid UserId { get; set; } + + [FromRoute] + public Guid NotificationId { get; set; } + + [FromQuery] + public string Status { get; set; } + + public UpdateNotificationStatus(Guid userId, Guid notificationId, string status) + { + UserId = userId; + NotificationId = notificationId; + Status = status; + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/ContractAttribute.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/ContractAttribute.cs new file mode 100644 index 00000000..8517813c --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/ContractAttribute.cs @@ -0,0 +1,7 @@ +namespace MiniSpace.Services.Notifications.Application +{ + public class ContractAttribute : Attribute + { + + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Dto/FriendDto.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Dto/FriendDto.cs new file mode 100644 index 00000000..94f1dbdf --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Dto/FriendDto.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Notifications.Application.Dto +{ + public class FriendDto + { + public Guid Id { get; set; } + public Guid StudentId { get; set; } + public Guid FriendId { get; set; } + public DateTime CreatedAt { get; set; } + public string FriendState { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Dto/FriendEventDto.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Dto/FriendEventDto.cs new file mode 100644 index 00000000..94fdff8d --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Dto/FriendEventDto.cs @@ -0,0 +1,12 @@ +namespace MiniSpace.Services.Notifications.Application.Dto +{ + public class FriendEventDto + { + public Guid Id { get; set; } + public Guid EventId { get; set; } + public Guid UserId { get; set; } + public string EventType { get; set; } + public string Details { get; set; } + public DateTime CreatedAt { get; set; } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Dto/FriendRequestDto.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Dto/FriendRequestDto.cs new file mode 100644 index 00000000..3db0d3ab --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Dto/FriendRequestDto.cs @@ -0,0 +1,15 @@ +using MiniSpace.Services.Notifications.Core.Entities; + +namespace MiniSpace.Services.Notifications.Application.Dto +{ + public class FriendRequestDto + { + public Guid Id { get; set; } + public Guid InviterId { get; set; } + public Guid InviteeId { get; set; } + public DateTime RequestedAt { get; set; } + public FriendState State { get; set; } + public Guid StudentId { get; set; } + + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Dto/NotificationDto.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Dto/NotificationDto.cs new file mode 100644 index 00000000..a44201b9 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Dto/NotificationDto.cs @@ -0,0 +1,17 @@ +using System; +using MiniSpace.Services.Notifications.Core.Entities; + +namespace MiniSpace.Services.Notifications.Application.Dto +{ + public class NotificationDto + { + public Guid NotificationId { get; set; } + public Guid UserId { get; set; } + public string Message { get; set; } + public string Status { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime? UpdatedAt { get; set; } + public Guid? RelatedEntityId { get; set; } + public NotificationEventType EventType { get; set; } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Dto/StudentDto.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Dto/StudentDto.cs new file mode 100644 index 00000000..edf4f13f --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Dto/StudentDto.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Notifications.Application.Dto +{ + public class StudentDto + { + public Guid Id { get; set; } + public string Email { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public int NumberOfFriends { get; set; } + public string ProfileImage { get; set; } + public string Description { get; set; } + public DateTime DateOfBirth { get; set; } + public bool EmailNotifications { get; set; } + public bool IsBanned { get; set; } + public bool IsOrganizer { get; set; } + public string State { get; set; } + public DateTime CreatedAt { get; set; } + public List InterestedInEvents { get; set; } + public List SignedUpEvents { get; set; } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Dto/StudentNotificationsDto.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Dto/StudentNotificationsDto.cs new file mode 100644 index 00000000..b261d875 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Dto/StudentNotificationsDto.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using MiniSpace.Services.Notifications.Core.Entities; + +namespace MiniSpace.Services.Notifications.Application.Dto +{ + public class StudentNotificationsDto + { + private List notifications = new List(); + public Guid StudentId { get; private set; } + + public StudentNotificationsDto(Guid studentId) + { + StudentId = studentId; + } + + public void AddNotification(Notification notification) + { + notifications.Add(notification); + } + + public IEnumerable Notifications => notifications.AsReadOnly(); + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/EventCreated.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/EventCreated.cs new file mode 100644 index 00000000..8eb5143a --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/EventCreated.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Notifications.Application.Events.External +{ + [Contract] + public class EventCreated(Guid eventId, Guid organizerId, IEnumerable mediaFilesIds) : IEvent + { + public Guid EventId { get; set; } = eventId; + public Guid OrganizerId { get; set; } = organizerId; + public IEnumerable MediaFilesIds { get; set; } = mediaFilesIds; + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/EventParticipantAdded.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/EventParticipantAdded.cs new file mode 100644 index 00000000..8e76f467 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/EventParticipantAdded.cs @@ -0,0 +1,20 @@ +using System; +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Notifications.Application.Events +{ + [Contract] + public class EventParticipantAdded: IEvent + { + public Guid EventId { get; } + public Guid ParticipantId { get; } + public string ParticipantName { get; } + + public EventParticipantAdded(Guid eventId, Guid participantId, string participantName) + { + EventId = eventId; + ParticipantId = participantId; + ParticipantName = participantName; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/EventParticipantRemoved.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/EventParticipantRemoved.cs new file mode 100644 index 00000000..389a94ff --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/EventParticipantRemoved.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Notifications.Application.Events +{ + [Contract] + public class EventParticipantRemoved: IEvent + { + public Guid EventId { get; } + public Guid Participant { get; } + + public EventParticipantRemoved(Guid eventId, Guid participant) + { + EventId = eventId; + Participant = participant; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/EventUpdated.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/EventUpdated.cs new file mode 100644 index 00000000..a38251ba --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/EventUpdated.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Notifications.Application.Events +{ + [Contract] + public class EventUpdated(Guid eventId, DateTime updatedAt, Guid updatedBy, IEnumerable mediaFilesIds) : IEvent + { + public Guid EventId { get; set; } = eventId; + public DateTime UpdatedAt { get; set; } = updatedAt; + public Guid UpdatedBy { get; set; } = updatedBy; + public IEnumerable MediaFilesIds { get; set; } = mediaFilesIds; + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/FriendAdded.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/FriendAdded.cs new file mode 100644 index 00000000..192bdaf5 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/FriendAdded.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.Notifications.Application.Events.External +{ + [Message("friends")] + public class FriendAdded : IEvent + { + public Guid RequesterId { get; } + public Guid FriendId { get; } + + public FriendAdded(Guid requesterId, Guid friendId) + { + RequesterId = requesterId; + FriendId = friendId; + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/FriendInvited.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/FriendInvited.cs new file mode 100644 index 00000000..f77e2329 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/FriendInvited.cs @@ -0,0 +1,19 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; +using MiniSpace.Services.Notifications.Core.Events; + +namespace MiniSpace.Services.Notifications.Application.Events.External +{ + [Contract] + public class FriendInvited : IEvent, IDomainEvent + { + public Guid InviterId { get; } + public Guid InviteeId { get; } + + public FriendInvited(Guid inviterId, Guid inviteeId) + { + InviterId = inviterId; + InviteeId = inviteeId; + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/FriendRequestCreated.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/FriendRequestCreated.cs new file mode 100644 index 00000000..14432b28 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/FriendRequestCreated.cs @@ -0,0 +1,19 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; +using System; + +namespace MiniSpace.Services.Notifications.Application.Events.External +{ + [Contract] + public class FriendRequestCreated : IEvent + { + public Guid RequesterId { get; } + public Guid FriendId { get; } + + public FriendRequestCreated(Guid requesterId, Guid friendId) + { + RequesterId = requesterId; + FriendId = friendId; + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/FriendRequestSent.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/FriendRequestSent.cs new file mode 100644 index 00000000..59205c79 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/FriendRequestSent.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.Notifications.Application.Events.External +{ + [Contract] + public class FriendRequestSent : IEvent + { + public Guid InviterId { get; } + public Guid InviteeId { get; } + + public FriendRequestSent(Guid inviterId, Guid inviteeId) + { + InviterId = inviterId; + InviteeId = inviteeId; + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/Handlers/EventCreatedHandler.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/Handlers/EventCreatedHandler.cs new file mode 100644 index 00000000..79eb752d --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/Handlers/EventCreatedHandler.cs @@ -0,0 +1,68 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Notifications.Core.Repositories; +using MiniSpace.Services.Notifications.Application.Services; +using MiniSpace.Services.Notifications.Core.Entities; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Threading; + +namespace MiniSpace.Services.Notifications.Application.Events.External.Handlers +{ + public class EventCreatedHandler : IEventHandler + { + private readonly IMessageBroker _messageBroker; + private readonly IStudentRepository _userRepository; + private readonly IStudentNotificationsRepository _studentNotificationsRepository; + + public EventCreatedHandler( + IMessageBroker messageBroker, + IStudentRepository userRepository, + IStudentNotificationsRepository studentNotificationsRepository) + { + _messageBroker = messageBroker; + _userRepository = userRepository; + _studentNotificationsRepository = studentNotificationsRepository; + } + + public async Task HandleAsync(EventCreated eventCreated, CancellationToken cancellationToken) + { + + var users = await _userRepository.GetAllAsync(); + + foreach (var user in users) + { + var notification = new Notification( + notificationId: Guid.NewGuid(), + userId: user.Id, + message: $"A new event has been created by Organizer {eventCreated.OrganizerId}", + status: NotificationStatus.Unread, + createdAt: DateTime.UtcNow, + updatedAt: null, + relatedEntityId: eventCreated.EventId, + eventType: NotificationEventType.NewEvent + ); + + + var studentNotifications = await _studentNotificationsRepository.GetByStudentIdAsync(user.Id); + if (studentNotifications == null) + { + studentNotifications = new StudentNotifications(user.Id); + } + + studentNotifications.AddNotification(notification); + await _studentNotificationsRepository.UpdateAsync(studentNotifications); + + // Publish the notification event + var notificationCreatedEvent = new NotificationCreated( + notificationId: notification.NotificationId, + userId: notification.UserId, + message: notification.Message, + createdAt: notification.CreatedAt + ); + + await _messageBroker.PublishAsync(notificationCreatedEvent); + } + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/Handlers/FriendAddedHandler.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/Handlers/FriendAddedHandler.cs new file mode 100644 index 00000000..7530b33a --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/Handlers/FriendAddedHandler.cs @@ -0,0 +1,36 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Notifications.Core.Repositories; +using MiniSpace.Services.Notifications.Application.Services; +using MiniSpace.Services.Notifications.Core.Entities; +using System.Collections.Generic; +using MiniSpace.Services.Notifications.Application.Exceptions; + +namespace MiniSpace.Services.Notifications.Application.Events.External.Handlers +{ + public class FriendAddedHandler : IEventHandler + { + private readonly INotificationRepository _notificationRepository; + private readonly IEventMapper _eventMapper; + private readonly IMessageBroker _messageBroker; + + public FriendAddedHandler(INotificationRepository notificationRepository, IEventMapper eventMapper, IMessageBroker messageBroker) + { + _notificationRepository = notificationRepository; + _eventMapper = eventMapper; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(NotificationCreated @event, CancellationToken cancellationToken) + { + var notification = await _notificationRepository.GetAsync(@event.NotificationId); + if (notification == null) + { + throw new NotificationNotFoundException(@event.NotificationId); + } + + await _notificationRepository.AddAsync(notification); + var events = _eventMapper.MapAll(notification.Events); + await _messageBroker.PublishAsync(events.ToArray()); + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/Handlers/FriendInvitedHandler.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/Handlers/FriendInvitedHandler.cs new file mode 100644 index 00000000..a7ff7ba9 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/Handlers/FriendInvitedHandler.cs @@ -0,0 +1,79 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Notifications.Core.Repositories; +using MiniSpace.Services.Notifications.Application.Services; +using MiniSpace.Services.Notifications.Core.Entities; +using System.Collections.Generic; +using MiniSpace.Services.Notifications.Application.Exceptions; +using MiniSpace.Services.Notifications.Application.Services.Clients; +using System.Text.Json; + +namespace MiniSpace.Services.Notifications.Application.Events.External.Handlers +{ + public class FriendInvitedHandler : IEventHandler + { + private readonly INotificationRepository _notificationRepository; + private readonly IStudentNotificationsRepository _studentNotificationsRepository; + private readonly IStudentsServiceClient _studentsServiceClient; + private readonly IEventMapper _eventMapper; + private readonly IMessageBroker _messageBroker; + + public FriendInvitedHandler( + INotificationRepository notificationRepository, + IStudentNotificationsRepository studentNotificationsRepository, + IStudentsServiceClient studentsServiceClient, + IEventMapper eventMapper, + IMessageBroker messageBroker + ) + { + _notificationRepository = notificationRepository; + _studentNotificationsRepository = studentNotificationsRepository; + _studentsServiceClient = studentsServiceClient; + _eventMapper = eventMapper; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(FriendInvited @event, CancellationToken cancellationToken) + { + var inviter = await _studentsServiceClient.GetAsync(@event.InviterId); + // var invitee = await _studentsServiceClient.GetAsync(@event.InviteeId); + + var notificationMessage = $"You have been invited by {inviter.FirstName} {inviter.LastName} to be friends."; + + var notification = new Notification( + notificationId: Guid.NewGuid(), + userId: @event.InviteeId, + message: notificationMessage, + status: NotificationStatus.Unread, + createdAt: DateTime.UtcNow, + updatedAt: null, + relatedEntityId: @event.InviterId, + eventType: NotificationEventType.NewFriendRequest + ); + + await _notificationRepository.AddAsync(notification); + + var studentNotifications = await _studentNotificationsRepository.GetByStudentIdAsync(@event.InviteeId); + if (studentNotifications == null) + { + studentNotifications = new StudentNotifications(@event.InviteeId); + } + else + { + // _logger.AddInformation($"Retrieved existing notifications for studentId {@event.InviterId}."); + } + + studentNotifications.AddNotification(notification); + + await _studentNotificationsRepository.UpdateAsync(studentNotifications); + + var notificationCreatedEvent = new NotificationCreated( + notificationId: notification.NotificationId, + userId: notification.UserId, + message: notification.Message, + createdAt: notification.CreatedAt + ); + + await _messageBroker.PublishAsync(notificationCreatedEvent); + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/Handlers/FriendRequestCreatedHandler.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/Handlers/FriendRequestCreatedHandler.cs new file mode 100644 index 00000000..69a51e4c --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/Handlers/FriendRequestCreatedHandler.cs @@ -0,0 +1,82 @@ +using System.Text.Json; +using Convey.CQRS.Events; +using Microsoft.Extensions.Logging; +using MiniSpace.Services.Notifications.Application.Services; +using MiniSpace.Services.Notifications.Application.Services.Clients; +using MiniSpace.Services.Notifications.Core.Entities; +using MiniSpace.Services.Notifications.Core.Events; +using MiniSpace.Services.Notifications.Core.Repositories; + +namespace MiniSpace.Services.Notifications.Application.Events.External.Handlers +{ + public class FriendRequestCreatedHandler : IEventHandler + { + private readonly IFriendEventRepository _friendEventRepository; + private readonly IStudentsServiceClient _studentsServiceClient; + private readonly INotificationRepository _notificationRepository; + private readonly IEventMapper _eventMapper; + private readonly IMessageBroker _messageBroker; + private readonly ILogger _logger; + + public FriendRequestCreatedHandler(IFriendEventRepository friendEventRepository, IStudentsServiceClient studentsServiceClient, IEventMapper eventMapper, IMessageBroker messageBroker, ILogger logger) + { + _friendEventRepository = friendEventRepository; + _studentsServiceClient = studentsServiceClient; + _eventMapper = eventMapper; + _messageBroker = messageBroker; + _logger = logger; + } + + public async Task HandleAsync(FriendRequestCreated friendEvent, CancellationToken cancellationToken) + { + _logger.LogInformation($"Received FriendRequestCreated event: RequesterId={friendEvent.RequesterId}, FriendId={friendEvent.FriendId}"); + + var requester = await _studentsServiceClient.GetAsync(friendEvent.RequesterId); + var friend = await _studentsServiceClient.GetAsync(friendEvent.FriendId); + + if (requester == null || friend == null) + { + _logger.LogError($"Failed to fetch student data for RequesterId={friendEvent.RequesterId} or FriendId={friendEvent.FriendId}"); + return; + } + + var eventDetails = $"A new friend request created from {requester.FirstName} {requester.LastName} to {friend.FirstName} {friend.LastName}"; + var notificationMessage = $"You have received a friend request from {requester.FirstName} {requester.LastName}"; + + var newFriendEvent = new FriendEvent( + id: Guid.NewGuid(), + eventId: Guid.NewGuid(), + userId: friendEvent.RequesterId, + friendId: friendEvent.FriendId, + eventType: "FriendRequestCreated", + details: eventDetails, + createdAt: DateTime.UtcNow + ); + + var notification = new Notification( + notificationId: Guid.NewGuid(), + userId: friendEvent.FriendId, + message: notificationMessage, + status: NotificationStatus.Unread, + createdAt: DateTime.UtcNow, + updatedAt: null, + relatedEntityId: friendEvent.RequesterId, + eventType: NotificationEventType.NewFriendRequest + ); + + await _friendEventRepository.AddAsync(newFriendEvent); + await _messageBroker.PublishAsync(friendEvent); + + _logger.LogInformation($"Stored new friend event for UserId={friendEvent.RequesterId} with details: {eventDetails}"); + var notificationCreated = new NotificationCreated( + notificationId: notification.NotificationId, + userId: notification.UserId, + message: notification.Message, + createdAt: notification.CreatedAt + ); + + await _messageBroker.PublishAsync(notificationCreated); + _logger.LogInformation($"Published NotificationCreated event for NotificationId={notification.NotificationId}"); + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/Handlers/FriendRequestSentHandler.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/Handlers/FriendRequestSentHandler.cs new file mode 100644 index 00000000..c28696ce --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/Handlers/FriendRequestSentHandler.cs @@ -0,0 +1,53 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Notifications.Core.Repositories; +using MiniSpace.Services.Notifications.Application.Services; +using MiniSpace.Services.Notifications.Core.Entities; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MiniSpace.Services.Notifications.Application.Exceptions; + +namespace MiniSpace.Services.Notifications.Application.Events.External.Handlers +{ + public class FriendRequestSentHandler : IEventHandler + { + private readonly INotificationRepository _notificationRepository; + private readonly IEventMapper _eventMapper; + private readonly IMessageBroker _messageBroker; + + public FriendRequestSentHandler(INotificationRepository notificationRepository, IEventMapper eventMapper, IMessageBroker messageBroker) + { + _notificationRepository = notificationRepository; + _eventMapper = eventMapper; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(FriendRequestSent @event, CancellationToken cancellationToken) + { + var notificationMessage = $"You have received a friend request."; + + var notification = new Notification( + notificationId: Guid.NewGuid(), + userId: @event.InviteeId, + message: notificationMessage, + status: NotificationStatus.Unread, + createdAt: DateTime.UtcNow, + updatedAt: null, + relatedEntityId: @event.InviterId, + eventType: NotificationEventType.NewFriendRequest + ); + + await _notificationRepository.AddAsync(notification); + + var notificationCreatedEvent = new NotificationCreated( + notificationId: notification.NotificationId, + userId: notification.UserId, + message: notification.Message, + createdAt: notification.CreatedAt + ); + + await _messageBroker.PublishAsync(notificationCreatedEvent); + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/Handlers/NotificationCreatedHandler.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/Handlers/NotificationCreatedHandler.cs new file mode 100644 index 00000000..ba112964 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/Handlers/NotificationCreatedHandler.cs @@ -0,0 +1,37 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Notifications.Core.Repositories; +using MiniSpace.Services.Notifications.Application.Services; +using MiniSpace.Services.Notifications.Core.Entities; +using System.Collections.Generic; +using MiniSpace.Services.Notifications.Application.Exceptions; + +namespace MiniSpace.Services.Notifications.Application.Events.External.Handlers +{ + public class NotificationCreatedHandler : IEventHandler + { + private readonly INotificationRepository _notificationRepository; + private readonly IEventMapper _eventMapper; + private readonly IMessageBroker _messageBroker; + + public NotificationCreatedHandler(INotificationRepository notificationRepository, IEventMapper eventMapper, IMessageBroker messageBroker) + { + _notificationRepository = notificationRepository; + _eventMapper = eventMapper; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(NotificationCreated @event, CancellationToken cancellationToken) + { + var notification = await _notificationRepository.GetAsync(@event.NotificationId); + if (notification == null) + { + throw new NotificationNotFoundException(@event.NotificationId); + } + + await _notificationRepository.AddAsync(notification); + + var events = _eventMapper.MapAll(notification.Events); + await _messageBroker.PublishAsync(events.ToArray()); + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/Handlers/NotificationDeletedHandler.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/Handlers/NotificationDeletedHandler.cs new file mode 100644 index 00000000..8ccbaff4 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/Handlers/NotificationDeletedHandler.cs @@ -0,0 +1,38 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Notifications.Core.Repositories; +using MiniSpace.Services.Notifications.Application.Services; +using System.Threading.Tasks; +using MiniSpace.Services.Notifications.Application.Exceptions; + +namespace MiniSpace.Services.Notifications.Application.Events.External.Handlers +{ + public class NotificationDeletedHandler : IEventHandler + { + private readonly INotificationRepository _notificationRepository; + private readonly IMessageBroker _messageBroker; + private readonly IEventMapper _eventMapper; + + public NotificationDeletedHandler(INotificationRepository notificationRepository, IMessageBroker messageBroker, IEventMapper eventMapper) + { + _notificationRepository = notificationRepository; + _messageBroker = messageBroker; + _eventMapper = eventMapper; + } + + public async Task HandleAsync(NotificationDeleted @event, CancellationToken cancellationToken) + { + var notification = await _notificationRepository.GetAsync(@event.NotificationId); + + if (notification == null) + { + throw new NotificationNotFoundException(@event.NotificationId); + } + + notification.MarkAsDeleted(); + await _notificationRepository.UpdateAsync(notification); + + var events = _eventMapper.MapAll(notification.Events); + await _messageBroker.PublishAsync(events.ToArray()); + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/Handlers/NotificationUpdatedHandler.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/Handlers/NotificationUpdatedHandler.cs new file mode 100644 index 00000000..5d65cca1 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/Handlers/NotificationUpdatedHandler.cs @@ -0,0 +1,49 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Notifications.Core.Repositories; +using MiniSpace.Services.Notifications.Core.Entities; +using MiniSpace.Services.Notifications.Application.Exceptions; +using MiniSpace.Services.Notifications.Application.Services; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Notifications.Application.Events.External.Handlers +{ + public class NotificationUpdatedHandler : IEventHandler + { + private readonly INotificationRepository _notificationRepository; + private readonly IMessageBroker _messageBroker; + private readonly IEventMapper _eventMapper; + + public NotificationUpdatedHandler(INotificationRepository notificationRepository, IMessageBroker messageBroker, IEventMapper eventMapper) + { + _notificationRepository = notificationRepository; + _messageBroker = messageBroker; + _eventMapper = eventMapper; + } + + public async Task HandleAsync(NotificationUpdated @event, CancellationToken cancellationToken) + { + var notification = await _notificationRepository.GetAsync(@event.NotificationId); + if (notification == null) + { + throw new NotificationNotFoundException(@event.NotificationId); + } + + switch (@event.NewStatus) + { + case NotificationStatus.Read: + notification.MarkAsRead(); + break; + case NotificationStatus.Deleted: + notification.MarkAsDeleted(); + break; + default: + throw new InvalidOperationException("Unsupported status update."); + } + + await _notificationRepository.UpdateAsync(notification); + + var events = _eventMapper.MapAll(notification.Events); + await _messageBroker.PublishAsync(events.ToArray()); + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/Handlers/PendingFriendRequestAcceptedHandler.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/Handlers/PendingFriendRequestAcceptedHandler.cs new file mode 100644 index 00000000..8265b3e8 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/Handlers/PendingFriendRequestAcceptedHandler.cs @@ -0,0 +1,77 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Notifications.Core.Repositories; +using MiniSpace.Services.Notifications.Core.Entities; +using MiniSpace.Services.Notifications.Application.Services.Clients; +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using MiniSpace.Services.Notifications.Application.Services; + +namespace MiniSpace.Services.Notifications.Application.Events.External.Handlers +{ + public class PendingFriendRequestAcceptedHandler : IEventHandler + { + private readonly IStudentNotificationsRepository _studentNotificationsRepository; + private readonly IMessageBroker _messageBroker; + private readonly IStudentsServiceClient _studentsServiceClient; + private readonly ILogger _logger; + + public PendingFriendRequestAcceptedHandler( + IStudentNotificationsRepository studentNotificationsRepository, + IMessageBroker messageBroker, + IStudentsServiceClient studentsServiceClient, + ILogger logger) + { + _studentNotificationsRepository = studentNotificationsRepository; + _messageBroker = messageBroker; + _studentsServiceClient = studentsServiceClient; + _logger = logger; + } + + public async Task HandleAsync(PendingFriendAccepted @event, CancellationToken cancellationToken) + { + var requester = await _studentsServiceClient.GetAsync(@event.RequesterId); + var friend = await _studentsServiceClient.GetAsync(@event.FriendId); + + if (requester == null || friend == null) + { + _logger.LogError($"Failed to fetch student data for RequesterId={@event.RequesterId} or FriendId={@event.FriendId}"); + return; + } + + var notificationMessage = $"Your friend request to {friend.FirstName} {friend.LastName} has been accepted."; + + var notification = new Notification( + notificationId: Guid.NewGuid(), + userId: @event.RequesterId, + message: notificationMessage, + status: NotificationStatus.Unread, + createdAt: DateTime.UtcNow, + updatedAt: null, + relatedEntityId: @event.FriendId, + eventType: NotificationEventType.FriendRequestAccepted + ); + + var studentNotifications = await _studentNotificationsRepository.GetByStudentIdAsync(@event.RequesterId); + if (studentNotifications == null) + { + studentNotifications = new StudentNotifications(@event.RequesterId); + _logger.LogInformation($"Creating new StudentNotifications for UserId={@event.RequesterId}"); + } + + studentNotifications.AddNotification(notification); + _logger.LogInformation($"Adding notification to StudentNotifications for UserId={@event.RequesterId}"); + + await _studentNotificationsRepository.AddOrUpdateAsync(studentNotifications); + _logger.LogInformation($"Updated StudentNotifications for UserId={@event.RequesterId}"); + + var notificationCreatedEvent = new NotificationCreated( + notificationId: notification.NotificationId, + userId: notification.UserId, + message: notification.Message, + createdAt: notification.CreatedAt + ); + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/NotificationCreated.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/NotificationCreated.cs new file mode 100644 index 00000000..b9cf205f --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/NotificationCreated.cs @@ -0,0 +1,23 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.Notifications.Application.Events.External +{ + [Contract] + public class NotificationCreated : IEvent + { + public Guid NotificationId { get; } + public Guid UserId { get; } + public string Message { get; } + + public DateTime CreatedAt { get; } + + public NotificationCreated(Guid notificationId, Guid userId, string message, DateTime createdAt) + { + NotificationId = notificationId; + UserId = userId; + Message = message; + CreatedAt = createdAt; + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/NotificationDeleted.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/NotificationDeleted.cs new file mode 100644 index 00000000..d10515dc --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/NotificationDeleted.cs @@ -0,0 +1,19 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.Notifications.Application.Events.External +{ + [Contract] + public class NotificationDeleted : IEvent + { + public Guid UserId { get; } + public Guid NotificationId { get; } + + + public NotificationDeleted(Guid userId, Guid notificationId) + { + UserId = userId; + NotificationId = notificationId; + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/NotificationUpdated.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/NotificationUpdated.cs new file mode 100644 index 00000000..a5145a1e --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/NotificationUpdated.cs @@ -0,0 +1,21 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; +using MiniSpace.Services.Notifications.Core.Entities; + +namespace MiniSpace.Services.Notifications.Application.Events.External +{ + [Contract] + public class NotificationUpdated : IEvent + { + public Guid NotificationId { get; } + public string UserId { get; } + public NotificationStatus NewStatus { get; } + + public NotificationUpdated(Guid notificationId, string userId, NotificationStatus newStatus) + { + NotificationId = notificationId; + UserId = userId; + NewStatus = newStatus; + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/PendingFriendAccepted.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/PendingFriendAccepted.cs new file mode 100644 index 00000000..1bcbb728 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/PendingFriendAccepted.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.Notifications.Application.Events.External +{ + [Contract] + public class PendingFriendAccepted : IEvent + { + public Guid RequesterId { get; } + public Guid FriendId { get; } + + public PendingFriendAccepted(Guid requesterId, Guid friendId) + { + RequesterId = requesterId; + FriendId = friendId; + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/PendingFriendDeclined.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/PendingFriendDeclined.cs new file mode 100644 index 00000000..df1e2fb5 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/PendingFriendDeclined.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.Notifications.Application.Events.External +{ + [Message("friends")] + public class PendingFriendDeclined : IEvent + { + public Guid RequesterId { get; } + public Guid FriendId { get; } + + public PendingFriendDeclined(Guid requesterId, Guid friendId) + { + RequesterId = requesterId; + FriendId = friendId; + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/StudentCancelledSignUpToEvent.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/StudentCancelledSignUpToEvent.cs new file mode 100644 index 00000000..e6426435 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/StudentCancelledSignUpToEvent.cs @@ -0,0 +1,19 @@ +using System; +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.Notifications.Application.Events +{ + [Contract] + public class StudentCancelledSignUpToEvent: IEvent + { + public Guid EventId { get; } + public Guid StudentId { get; } + + public StudentCancelledSignUpToEvent(Guid eventId, Guid studentId) + { + EventId = eventId; + StudentId = studentId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/StudentShowedInterestInEvent.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/StudentShowedInterestInEvent.cs new file mode 100644 index 00000000..6ef69e03 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/StudentShowedInterestInEvent.cs @@ -0,0 +1,18 @@ +using System; +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Notifications.Application.Events +{ + [Contract] + public class StudentShowedInterestInEvent: IEvent + { + public Guid EventId { get; } + public Guid StudentId { get; } + + public StudentShowedInterestInEvent(Guid eventId, Guid studentId) + { + EventId = eventId; + StudentId = studentId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/StudentSignedUpToEvent.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/StudentSignedUpToEvent.cs new file mode 100644 index 00000000..23f03f88 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/StudentSignedUpToEvent.cs @@ -0,0 +1,18 @@ +using System; +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Notifications.Application.Events +{ + [Contract] + public class StudentSignedUpToEvent: IEvent + { + public Guid EventId { get; } + public Guid StudentId { get; } + + public StudentSignedUpToEvent(Guid eventId, Guid studentId) + { + EventId = eventId; + StudentId = studentId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/Rejected/NotificationCreationRejected.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/Rejected/NotificationCreationRejected.cs new file mode 100644 index 00000000..fb1249a7 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/Rejected/NotificationCreationRejected.cs @@ -0,0 +1,19 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Notifications.Application.Events.Rejected +{ + + public class NotificationCreationRejected : IRejectedEvent + { + public Guid NotificationId { get; } + public string Reason { get; } + public string Code { get; } + + public NotificationCreationRejected(Guid notificationId, string reason, string code) + { + NotificationId = notificationId; + Reason = reason; + Code = code; + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/Rejected/NotificationDeletionRejected.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/Rejected/NotificationDeletionRejected.cs new file mode 100644 index 00000000..91946de4 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/Rejected/NotificationDeletionRejected.cs @@ -0,0 +1,19 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Notifications.Application.Events.Rejected +{ + + public class NotificationDeletionRejected : IRejectedEvent + { + public Guid NotificationId { get; } + public string Reason { get; } + public string Code { get; } + + public NotificationDeletionRejected(Guid notificationId, string reason, string code) + { + NotificationId = notificationId; + Reason = reason; + Code = code; + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/Rejected/NotificationProcessRejected.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/Rejected/NotificationProcessRejected.cs new file mode 100644 index 00000000..1cbfaac2 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/Rejected/NotificationProcessRejected.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Notifications.Application.Events.Rejected +{ + public class NotificationProcessRejected : IRejectedEvent + { + public Guid NotificationId { get; } + public string Reason { get; } + public string Code { get; } + + public NotificationProcessRejected(Guid notificationId, string reason, string code) + { + NotificationId = notificationId; + Reason = reason; + Code = code; + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/Rejected/NotificationUpdateRejected.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/Rejected/NotificationUpdateRejected.cs new file mode 100644 index 00000000..4ca74c88 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/Rejected/NotificationUpdateRejected.cs @@ -0,0 +1,19 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Notifications.Application.Events.Rejected +{ + + public class NotificationUpdateRejected : IRejectedEvent + { + public Guid NotificationId { get; } + public string Reason { get; } + public string Code { get; } + + public NotificationUpdateRejected(Guid notificationId, string reason, string code) + { + NotificationId = notificationId; + Reason = reason; + Code = code; + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Exceptions/AppException.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Exceptions/AppException.cs new file mode 100644 index 00000000..a282b433 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Exceptions/AppException.cs @@ -0,0 +1,11 @@ +namespace MiniSpace.Services.Notifications.Application.Exceptions +{ + public class AppException : Exception + { + public virtual string Code { get; } + + protected AppException(string message) : base(message) + { + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Exceptions/InvalidNotificationStatusException.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Exceptions/InvalidNotificationStatusException.cs new file mode 100644 index 00000000..2b3c57b2 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Exceptions/InvalidNotificationStatusException.cs @@ -0,0 +1,14 @@ +using System; + +namespace MiniSpace.Services.Notifications.Application.Exceptions +{ + public class InvalidNotificationStatusException : AppException + { + public override string Code { get; } = "invalid_notification_status"; + + public InvalidNotificationStatusException(string message) + : base(message) + { + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Exceptions/NotificationAlreadyDeletedException.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Exceptions/NotificationAlreadyDeletedException.cs new file mode 100644 index 00000000..d84441ca --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Exceptions/NotificationAlreadyDeletedException.cs @@ -0,0 +1,14 @@ +namespace MiniSpace.Services.Notifications.Application.Exceptions +{ + public class NotificationAlreadyDeletedException : AppException + { + public override string Code { get; } = "notification_already_deleted"; + public Guid NotificationId { get; } + + public NotificationAlreadyDeletedException(Guid notificationId) + : base($"Notification with ID: {notificationId} has already been deleted.") + { + NotificationId = notificationId; + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Exceptions/NotificationNotFoundException.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Exceptions/NotificationNotFoundException.cs new file mode 100644 index 00000000..3c961c7d --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Exceptions/NotificationNotFoundException.cs @@ -0,0 +1,21 @@ +namespace MiniSpace.Services.Notifications.Application.Exceptions +{ + public class NotificationNotFoundException : AppException + { + public override string Code { get; } = "notification_not_found"; + public Guid NotificationId { get; } + + + public NotificationNotFoundException(Guid notificationId) + : base($"Notification with ID: {notificationId} was not found.") + { + NotificationId = notificationId; + } + + public NotificationNotFoundException(Guid userId, string message) + : base(message) + { + NotificationId = userId; + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Extensions.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Extensions.cs new file mode 100644 index 00000000..893e20fa --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Extensions.cs @@ -0,0 +1,16 @@ +using Convey; +using Convey.CQRS.Commands; +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Notifications.Application +{ + public static class Extensions + { + public static IConveyBuilder AddApplication(this IConveyBuilder builder) + => builder + .AddCommandHandlers() + .AddEventHandlers() + .AddInMemoryCommandDispatcher() + .AddInMemoryEventDispatcher(); + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/IAppContext.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/IAppContext.cs new file mode 100644 index 00000000..64264dc9 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/IAppContext.cs @@ -0,0 +1,8 @@ +namespace MiniSpace.Services.Notifications.Application +{ + public interface IAppContext + { + string RequestId { get; } + IIdentityContext Identity { get; } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/IIdentityContext.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/IIdentityContext.cs new file mode 100644 index 00000000..c7bd6f1e --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/IIdentityContext.cs @@ -0,0 +1,15 @@ +namespace MiniSpace.Services.Notifications.Application +{ + public interface IIdentityContext + { + Guid Id { get; } + string Role { get; } + string Name { get; } + string Email { get; } + bool IsAuthenticated { get; } + bool IsAdmin { get; } + bool IsBanned { get; } + bool IsOrganizer { get; } + IDictionary Claims { get; } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/MiniSpace.Services.Notifications.Application.csproj b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/MiniSpace.Services.Notifications.Application.csproj new file mode 100644 index 00000000..95b3cf27 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/MiniSpace.Services.Notifications.Application.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + enable + disable + + + + + + + + + + + + + + + diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/MiniSpace.Services.Notifications.Application.sln b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/MiniSpace.Services.Notifications.Application.sln new file mode 100644 index 00000000..e104357f --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/MiniSpace.Services.Notifications.Application.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniSpace.Services.Notifications.Application", "MiniSpace.Services.Notifications.Application.csproj", "{85F8B10B-A401-4C70-9202-F2A8C1C7AF61}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {85F8B10B-A401-4C70-9202-F2A8C1C7AF61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {85F8B10B-A401-4C70-9202-F2A8C1C7AF61}.Debug|Any CPU.Build.0 = Debug|Any CPU + {85F8B10B-A401-4C70-9202-F2A8C1C7AF61}.Release|Any CPU.ActiveCfg = Release|Any CPU + {85F8B10B-A401-4C70-9202-F2A8C1C7AF61}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {AE2AFDD2-927B-4122-835F-837E29C3330F} + EndGlobalSection +EndGlobal diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Queries/GetNotification.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Queries/GetNotification.cs new file mode 100644 index 00000000..7470403d --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Queries/GetNotification.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Queries; +using MiniSpace.Services.Notifications.Application.Dto; +using System; + +namespace MiniSpace.Services.Notifications.Application.Queries +{ + public class GetNotification : IQuery + { + public Guid UserId { get; set; } + public Guid NotificationId { get; set; } + + public GetNotification(Guid userId, Guid notificationId) + { + UserId = userId; + NotificationId = notificationId; + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Queries/GetNotificationsByUser.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Queries/GetNotificationsByUser.cs new file mode 100644 index 00000000..e1b100a1 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Queries/GetNotificationsByUser.cs @@ -0,0 +1,19 @@ +using Convey.CQRS.Queries; +using MiniSpace.Services.Notifications.Application.Dto; +using MiniSpace.Services.Notifications.Core.Entities; +using System; + +namespace MiniSpace.Services.Notifications.Application.Queries +{ + public class GetNotificationsByUser : IQuery>, IPagedNotificationsQuery + { + public Guid UserId { get; set; } + public DateTime? StartDate { get; set; } + public DateTime? EndDate { get; set; } + public string Status { get; set; } + public int Page { get; set; } = 1; + public int ResultsPerPage { get; set; } = 10; + public string OrderBy { get; set; } = "CreatedAt"; + public string SortOrder { get; set; } = "desc"; + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Queries/IPagedNotificationsQuery.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Queries/IPagedNotificationsQuery.cs new file mode 100644 index 00000000..bea46741 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Queries/IPagedNotificationsQuery.cs @@ -0,0 +1,35 @@ +using Convey.CQRS.Queries; +using System.Collections.Generic; + +namespace MiniSpace.Services.Notifications.Application.Queries +{ + public interface IPagedNotificationsQuery : IPagedQuery + { + new int Page { get; set; } + new int ResultsPerPage { get; set; } + new string OrderBy { get; set; } + new string SortOrder { get; set; } + } + + public class PagedResult + { + public List Results { get; set; } + public int Total { get; set; } + public int PageSize { get; set; } + public int Page { get; set; } + public string NextPage { get; set; } + public string PrevPage { get; set; } + + public PagedResult(List results, int total, int pageSize, int page, string baseUrl) + { + Results = results; + Total = total; + PageSize = pageSize; + Page = page; + + int totalPages = (int)Math.Ceiling(total / (double)pageSize); + NextPage = page < totalPages ? $"{baseUrl}?page={page + 1}&resultsPerPage={pageSize}" : null; + PrevPage = page > 1 ? $"{baseUrl}?page={page - 1}&resultsPerPage={pageSize}" : null; + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Queries/IPagedQuery.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Queries/IPagedQuery.cs new file mode 100644 index 00000000..adc18ac8 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Queries/IPagedQuery.cs @@ -0,0 +1,12 @@ +using Convey.CQRS.Queries; + +namespace MiniSpace.Services.Notifications.Application.Queries +{ + public interface IPagedQuery + { + int Page { get; set; } + int ResultsPerPage { get; set; } + string OrderBy { get; set; } + string SortOrder { get; set; } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Services/Clients/IEventsServiceClinet.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Services/Clients/IEventsServiceClinet.cs new file mode 100644 index 00000000..e69de29b diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Services/Clients/IFriendServiceClient.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Services/Clients/IFriendServiceClient.cs new file mode 100644 index 00000000..ef577925 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Services/Clients/IFriendServiceClient.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Notifications.Application.Dto; + +namespace MiniSpace.Services.Notifications.Application.Services.Clients +{ + public interface IFriendsServiceClient + { + Task> GetFriendsAsync(Guid studentId); + Task> GetRequestsAsync(Guid studentId); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Services/Clients/IStudentsServiceClient.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Services/Clients/IStudentsServiceClient.cs new file mode 100644 index 00000000..d78972b3 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Services/Clients/IStudentsServiceClient.cs @@ -0,0 +1,12 @@ +using System; +using System.Threading.Tasks; +using MiniSpace.Services.Notifications.Application.Dto; + +namespace MiniSpace.Services.Notifications.Application.Services.Clients +{ + public interface IStudentsServiceClient + { + Task GetAsync(Guid id); + public Task> GetAllAsync(); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Services/IDateTimeProvider.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Services/IDateTimeProvider.cs new file mode 100644 index 00000000..9b106932 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Services/IDateTimeProvider.cs @@ -0,0 +1,7 @@ +namespace MiniSpace.Services.Notifications.Application.Services +{ + public interface IDateTimeProvider + { + DateTime Now { get; } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Services/IEventMapper.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Services/IEventMapper.cs new file mode 100644 index 00000000..73e38bb4 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Services/IEventMapper.cs @@ -0,0 +1,11 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Notifications.Core.Events; + +namespace MiniSpace.Services.Notifications.Application.Services +{ + public interface IEventMapper + { + IEvent Map(IDomainEvent @event); + IEnumerable MapAll(IEnumerable events); + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Services/IMessageBroker.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Services/IMessageBroker.cs new file mode 100644 index 00000000..894dce62 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Services/IMessageBroker.cs @@ -0,0 +1,10 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Notifications.Application.Services +{ + public interface IMessageBroker + { + Task PublishAsync(params IEvent[] events); + Task PublishAsync(IEnumerable events); + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Entities/AggregateId.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Entities/AggregateId.cs new file mode 100644 index 00000000..e79d7b68 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Entities/AggregateId.cs @@ -0,0 +1,50 @@ +using MiniSpace.Services.Notifications.Core.Exceptions; + +namespace MiniSpace.Services.Notifications.Core.Entities +{ + public class AggregateId : IEquatable + { + public Guid Value { get; } + + public AggregateId() + { + Value = Guid.NewGuid(); + } + + public AggregateId(Guid value) + { + if (value == Guid.Empty) + { + throw new InvalidAggregateIdException(); + } + + Value = value; + } + + public bool Equals(AggregateId other) + { + if (ReferenceEquals(null, other)) return false; + return ReferenceEquals(this, other) || Value.Equals(other.Value); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + return obj.GetType() == GetType() && Equals((AggregateId) obj); + } + + public override int GetHashCode() + { + return Value.GetHashCode(); + } + + public static implicit operator Guid(AggregateId id) + => id.Value; + + public static implicit operator AggregateId(Guid id) + => new AggregateId(id); + + public override string ToString() => Value.ToString(); + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Entities/AggregateRoot.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Entities/AggregateRoot.cs new file mode 100644 index 00000000..196c10fe --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Entities/AggregateRoot.cs @@ -0,0 +1,19 @@ +using MiniSpace.Services.Notifications.Core.Events; + +namespace MiniSpace.Services.Notifications.Core.Entities +{ + public abstract class AggregateRoot + { + private readonly List _events = new List(); + public IEnumerable Events => _events; + public AggregateId Id { get; protected set; } + public int Version { get; protected set; } + + protected void AddEvent(IDomainEvent @event) + { + _events.Add(@event); + } + + public void ClearEvents() => _events.Clear(); + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Entities/FriendEvent.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Entities/FriendEvent.cs new file mode 100644 index 00000000..d480b00a --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Entities/FriendEvent.cs @@ -0,0 +1,24 @@ +namespace MiniSpace.Services.Notifications.Core.Entities +{ + public class FriendEvent + { + public Guid Id { get; private set; } + public Guid EventId { get; private set; } + public Guid UserId { get; private set; } + public Guid FriendId { get; private set;} + public string EventType { get; private set; } + public string Details { get; private set; } + public DateTime CreatedAt { get; private set; } + + public FriendEvent(Guid id, Guid eventId, Guid userId, Guid friendId, string eventType, string details, DateTime createdAt) + { + Id = id; + EventId = eventId; + UserId = userId; + FriendId = friendId; + EventType = eventType; + Details = details; + CreatedAt = createdAt; + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Entities/FriendState.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Entities/FriendState.cs new file mode 100644 index 00000000..f0664cbf --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Entities/FriendState.cs @@ -0,0 +1,13 @@ +namespace MiniSpace.Services.Notifications.Core.Entities +{ + public enum FriendState + { + Unknown, + Requested, + Accepted, + Declined, + Blocked, + Cancelled, + Confirmed + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Entities/Notification.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Entities/Notification.cs new file mode 100644 index 00000000..06363b98 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Entities/Notification.cs @@ -0,0 +1,51 @@ +using System; + +namespace MiniSpace.Services.Notifications.Core.Entities +{ + public class Notification : AggregateRoot + { + public Guid NotificationId { get; set; } + public Guid UserId { get; set; } + public string Message { get; set; } + public NotificationStatus Status { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime? UpdatedAt { get; set; } + public Guid? RelatedEntityId { get; set; } + public NotificationEventType EventType { get; set; } + + public Notification(Guid notificationId, + Guid userId, + string message, + NotificationStatus status, + DateTime createdAt, + DateTime? updatedAt, + NotificationEventType eventType, + Guid? relatedEntityId = null + ) + { + NotificationId = notificationId; + UserId = userId; + Message = message; + Status = status; + CreatedAt = createdAt; + UpdatedAt = updatedAt; + EventType = eventType; + RelatedEntityId = relatedEntityId; + } + + public void MarkAsRead() + { + if (Status != NotificationStatus.Deleted) + { + Status = NotificationStatus.Read; + UpdatedAt = DateTime.UtcNow; + } + } + + public void MarkAsDeleted() + { + Status = NotificationStatus.Deleted; + UpdatedAt = DateTime.UtcNow; + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Entities/NotificationEventType.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Entities/NotificationEventType.cs new file mode 100644 index 00000000..d5767d8e --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Entities/NotificationEventType.cs @@ -0,0 +1,13 @@ +namespace MiniSpace.Services.Notifications.Core.Entities +{ + public enum NotificationEventType + { + NewFriendRequest, + NewPost, + NewEvent, + FriendRequestAccepted, + MentionedInPost, + EventReminder, + Other + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Entities/NotificationStatus.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Entities/NotificationStatus.cs new file mode 100644 index 00000000..fdc60dda --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Entities/NotificationStatus.cs @@ -0,0 +1,10 @@ +namespace MiniSpace.Services.Notifications.Core.Entities +{ + public enum NotificationStatus + { + Created, + Read, + Unread, + Deleted + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Entities/Student.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Entities/Student.cs new file mode 100644 index 00000000..ea35dc53 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Entities/Student.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Notifications.Core.Entities +{ + public class Student + { + public Guid Id { get; set; } + public string Email { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public int NumberOfFriends { get; set; } + public string ProfileImage { get; set; } + public string Description { get; set; } + public DateTime DateOfBirth { get; set; } + public bool EmailNotifications { get; set; } + public bool IsBanned { get; set; } + public bool IsOrganizer { get; set; } + public string State { get; set; } + public DateTime CreatedAt { get; set; } + public List InterestedInEvents { get; set; } + public List SignedUpEvents { get; set; } + + public Student(Guid id, string email, string firstName, string lastName, int numberOfFriends, string profileImage, string description, DateTime dateOfBirth, bool emailNotifications, bool isBanned, bool isOrganizer, string state, DateTime createdAt) + { + Id = id; + Email = email; + FirstName = firstName; + LastName = lastName; + NumberOfFriends = numberOfFriends; + ProfileImage = profileImage; + Description = description; + DateOfBirth = dateOfBirth; + EmailNotifications = emailNotifications; + IsBanned = isBanned; + IsOrganizer = isOrganizer; + State = state; + CreatedAt = createdAt; + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Entities/StudentNotifications.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Entities/StudentNotifications.cs new file mode 100644 index 00000000..81f2f05c --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Entities/StudentNotifications.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MiniSpace.Services.Notifications.Core.Entities +{ + public class StudentNotifications + { + public Guid StudentId { get; private set; } + private List _notifications; + + public IEnumerable Notifications => _notifications.AsReadOnly(); + + public StudentNotifications(Guid studentId) + { + StudentId = studentId; + _notifications = new List(); + } + + public void AddNotification(Notification notification) + { + if (notification != null) + { + _notifications.Add(notification); + } + } + + public void RemoveNotification(Guid notificationId) + { + _notifications.RemoveAll(n => n.NotificationId == notificationId); + } + + public void MarkNotificationAsRead(Guid notificationId) + { + var notification = _notifications.FirstOrDefault(n => n.NotificationId == notificationId); + if (notification != null) + { + notification.MarkAsRead(); + } + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Events/IDomainEvent.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Events/IDomainEvent.cs new file mode 100644 index 00000000..01c3aaf1 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Events/IDomainEvent.cs @@ -0,0 +1,7 @@ +namespace MiniSpace.Services.Notifications.Core.Events +{ + public interface IDomainEvent + { + + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Events/NotificationCreated.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Events/NotificationCreated.cs new file mode 100644 index 00000000..a9e3aa36 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Events/NotificationCreated.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Notifications.Core.Events +{ + public class NotificationCreated : IDomainEvent + { + public Guid NotificationId { get; } + public Guid UserId { get; } + public string Message { get; } + public DateTime CreatedAt { get; } + public NotificationCreated(Guid notificationId, Guid userId, string message, DateTime createdAt) + { + NotificationId = notificationId; + UserId = userId; + Message = message; + CreatedAt = createdAt; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Events/NotificationDeleted.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Events/NotificationDeleted.cs new file mode 100644 index 00000000..49ec07aa --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Events/NotificationDeleted.cs @@ -0,0 +1,16 @@ +using System; + +namespace MiniSpace.Services.Notifications.Core.Events +{ + public class NotificationDeleted : IDomainEvent + { + public Guid UserId { get; } + public Guid NotificationId { get; } + + public NotificationDeleted(Guid userId, Guid notificationId) + { + UserId = userId; + NotificationId = notificationId; + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Events/NotificationUpdated.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Events/NotificationUpdated.cs new file mode 100644 index 00000000..50b49f29 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Events/NotificationUpdated.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using MiniSpace.Services.Notifications.Core.Entities; + +namespace MiniSpace.Services.Notifications.Core.Events +{ + public class NotificationUpdated : IDomainEvent + { + public Guid NotificationId { get; } + public string UserId { get; } + public NotificationStatus NewStatus { get; } + + public NotificationUpdated(Guid notificationId, string userId, NotificationStatus newStatus) + { + NotificationId = notificationId; + UserId = userId; + NewStatus = newStatus; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Exceptions/DomainException.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Exceptions/DomainException.cs new file mode 100644 index 00000000..4b536127 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Exceptions/DomainException.cs @@ -0,0 +1,11 @@ +namespace MiniSpace.Services.Notifications.Core.Exceptions +{ + public abstract class DomainException : Exception + { + public virtual string Code { get; } + + protected DomainException(string message) : base(message) + { + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Exceptions/InvalidAggregateIdException.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Exceptions/InvalidAggregateIdException.cs new file mode 100644 index 00000000..d3b5dc10 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Exceptions/InvalidAggregateIdException.cs @@ -0,0 +1,11 @@ +namespace MiniSpace.Services.Notifications.Core.Exceptions +{ + public class InvalidAggregateIdException : DomainException + { + public override string Code { get; } = "invalid_aggregate_id"; + + public InvalidAggregateIdException() : base($"Invalid aggregate id.") + { + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Exceptions/InvalidStudentDateOfBirthException.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Exceptions/InvalidStudentDateOfBirthException.cs new file mode 100644 index 00000000..5b4a9be1 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Exceptions/InvalidStudentDateOfBirthException.cs @@ -0,0 +1,19 @@ +namespace MiniSpace.Services.Notifications.Core.Exceptions +{ + public class InvalidStudentDateOfBirthException : DomainException + { + public override string Code { get; } = "invalid_student_date_of_birth"; + public Guid Id; + public DateTime DateOfBirth { get; } + public DateTime Now { get; } + + public InvalidStudentDateOfBirthException(Guid id, DateTime dateOfBirth, DateTime now) : base( + $"Student with id: {id} has invalid date of birth. Today date: " + + $"'{now}' must be greater than date of birth: '{dateOfBirth}'.") + { + Id = id; + DateOfBirth = dateOfBirth; + Now = now; + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Exceptions/InvalidStudentDescriptionException.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Exceptions/InvalidStudentDescriptionException.cs new file mode 100644 index 00000000..4f6b213f --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Exceptions/InvalidStudentDescriptionException.cs @@ -0,0 +1,16 @@ +namespace MiniSpace.Services.Notifications.Core.Exceptions +{ + public class InvalidStudentDescriptionException : DomainException + { + public override string Code { get; } = "invalid_student_description"; + public Guid Id { get; } + public string Description { get; } + + public InvalidStudentDescriptionException(Guid id, string description) : base( + $"Student with id: {id} has invalid description.") + { + Id = id; + Description = description; + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/MiniSpace.Services.Notifications.Core.csproj b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/MiniSpace.Services.Notifications.Core.csproj new file mode 100644 index 00000000..cf309aa8 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/MiniSpace.Services.Notifications.Core.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + disable + + + diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/MiniSpace.Services.Notifications.Core.sln b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/MiniSpace.Services.Notifications.Core.sln new file mode 100644 index 00000000..64c96992 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/MiniSpace.Services.Notifications.Core.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniSpace.Services.Notifications.Core", "MiniSpace.Services.Notifications.Core.csproj", "{42E0A571-69FB-45ED-BACA-E0F72B9C0DE1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {42E0A571-69FB-45ED-BACA-E0F72B9C0DE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {42E0A571-69FB-45ED-BACA-E0F72B9C0DE1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {42E0A571-69FB-45ED-BACA-E0F72B9C0DE1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {42E0A571-69FB-45ED-BACA-E0F72B9C0DE1}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {0A93A2FC-8B65-4C19-A90B-588F58354A02} + EndGlobalSection +EndGlobal diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Repositories/IFriendEventRepository.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Repositories/IFriendEventRepository.cs new file mode 100644 index 00000000..4bd7f419 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Repositories/IFriendEventRepository.cs @@ -0,0 +1,14 @@ +using MiniSpace.Services.Notifications.Core.Entities; +using System; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Notifications.Core.Repositories +{ + public interface IFriendEventRepository + { + Task GetAsync(Guid id); + Task AddAsync(FriendEvent friendEvent); + Task UpdateAsync(FriendEvent friendEvent); + Task DeleteAsync(Guid id); + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Repositories/INotificationRepository.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Repositories/INotificationRepository.cs new file mode 100644 index 00000000..ca145af8 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Repositories/INotificationRepository.cs @@ -0,0 +1,12 @@ +using MiniSpace.Services.Notifications.Core.Entities; + +namespace MiniSpace.Services.Notifications.Core.Repositories +{ + public interface INotificationRepository + { + Task GetAsync(Guid id); + Task AddAsync(Notification notification); + Task UpdateAsync(Notification notification); + Task DeleteAsync(Guid id); + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Repositories/IStudentNotificationsRepository.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Repositories/IStudentNotificationsRepository.cs new file mode 100644 index 00000000..07192644 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Repositories/IStudentNotificationsRepository.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Notifications.Core.Entities; + +namespace MiniSpace.Services.Notifications.Core.Repositories +{ + public interface IStudentNotificationsRepository + { + Task GetByStudentIdAsync(Guid studentId); + Task AddAsync(StudentNotifications studentNotifications); + Task UpdateAsync(StudentNotifications studentNotifications); + Task AddOrUpdateAsync(StudentNotifications studentNotifications); + Task DeleteAsync(Guid studentId); + Task UpdateNotificationStatus(Guid studentId, Guid notificationId, string newStatus); + Task NotificationExists(Guid studentId, Guid notificationId); + Task DeleteNotification(Guid studentId, Guid notificationId); + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Repositories/IStudentRepository.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Repositories/IStudentRepository.cs new file mode 100644 index 00000000..fdb050ba --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Repositories/IStudentRepository.cs @@ -0,0 +1,13 @@ +using MiniSpace.Services.Notifications.Core.Entities; + +namespace MiniSpace.Services.Notifications.Core.Repositories +{ + public interface IStudentRepository + { + Task GetAsync(Guid id); + Task> GetAllAsync(); + Task AddAsync(Student student); + Task UpdateAsync(Student student); + Task DeleteAsync(Guid id); + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Contexts/AppContext.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Contexts/AppContext.cs new file mode 100644 index 00000000..f94c6406 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Contexts/AppContext.cs @@ -0,0 +1,27 @@ +using MiniSpace.Services.Notifications.Application; + +namespace MiniSpace.Services.Notifications.Infrastructure.Contexts +{ + internal class AppContext : IAppContext + { + public string RequestId { get; } + public IIdentityContext Identity { get; } + + internal AppContext() : this(Guid.NewGuid().ToString("N"), IdentityContext.Empty) + { + } + + internal AppContext(CorrelationContext context) : this(context.CorrelationId, + context.User is null ? IdentityContext.Empty : new IdentityContext(context.User)) + { + } + + internal AppContext(string requestId, IIdentityContext identity) + { + RequestId = requestId; + Identity = identity; + } + + internal static IAppContext Empty => new AppContext(); + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Contexts/AppContextFactory.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Contexts/AppContextFactory.cs new file mode 100644 index 00000000..770b5d3f --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Contexts/AppContextFactory.cs @@ -0,0 +1,36 @@ +using Convey.MessageBrokers; +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; +using MiniSpace.Services.Notifications.Application; +using MiniSpace.Services.Notifications.Infrastructure; + +namespace MiniSpace.Services.Notifications.Infrastructure.Contexts +{ + internal sealed class AppContextFactory : IAppContextFactory + { + private readonly ICorrelationContextAccessor _contextAccessor; + private readonly IHttpContextAccessor _httpContextAccessor; + + public AppContextFactory(ICorrelationContextAccessor contextAccessor, IHttpContextAccessor httpContextAccessor) + { + _contextAccessor = contextAccessor; + _httpContextAccessor = httpContextAccessor; + } + + public IAppContext Create() + { + if (_contextAccessor.CorrelationContext is { }) + { + var payload = JsonConvert.SerializeObject(_contextAccessor.CorrelationContext); + + return string.IsNullOrWhiteSpace(payload) + ? AppContext.Empty + : new AppContext(JsonConvert.DeserializeObject(payload)); + } + + var context = _httpContextAccessor.GetCorrelationContext(); + + return context is null ? AppContext.Empty : new AppContext(context); + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Contexts/CorrelationContext.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Contexts/CorrelationContext.cs new file mode 100644 index 00000000..d55b96c9 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Contexts/CorrelationContext.cs @@ -0,0 +1,22 @@ +namespace MiniSpace.Services.Notifications.Infrastructure.Contexts +{ + internal class CorrelationContext + { + public string CorrelationId { get; set; } + public string SpanContext { get; set; } + public UserContext User { get; set; } + public string ResourceId { get; set; } + public string TraceId { get; set; } + public string ConnectionId { get; set; } + public string Name { get; set; } + public DateTime CreatedAt { get; set; } + + public class UserContext + { + public string Id { get; set; } + public bool IsAuthenticated { get; set; } + public string Role { get; set; } + public IDictionary Claims { get; set; } + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Contexts/IdentityContext.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Contexts/IdentityContext.cs new file mode 100644 index 00000000..8628ec08 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Contexts/IdentityContext.cs @@ -0,0 +1,41 @@ +using MiniSpace.Services.Notifications.Application; + +namespace MiniSpace.Services.Notifications.Infrastructure.Contexts +{ + internal class IdentityContext : IIdentityContext + { + public Guid Id { get; } + public string Role { get; } = string.Empty; + public string Name { get; } = string.Empty; + public string Email { get; } = string.Empty; + public bool IsAuthenticated { get; } + public bool IsAdmin { get; } + public bool IsBanned { get; } + public bool IsOrganizer { get; } + public IDictionary Claims { get; } = new Dictionary(); + + internal IdentityContext() + { + } + + internal IdentityContext(CorrelationContext.UserContext context) + : this(context.Id, context.Role, context.IsAuthenticated, context.Claims) + { + } + + internal IdentityContext(string id, string role, bool isAuthenticated, IDictionary claims) + { + Id = Guid.TryParse(id, out var userId) ? userId : Guid.Empty; + Role = role ?? string.Empty; + IsAuthenticated = isAuthenticated; + IsAdmin = Role.Equals("admin", StringComparison.InvariantCultureIgnoreCase); + IsBanned = Role.Equals("banned", StringComparison.InvariantCultureIgnoreCase); + IsOrganizer = Role.Equals("organizer", StringComparison.InvariantCultureIgnoreCase); + Claims = claims ?? new Dictionary(); + Name = Claims.TryGetValue("name", out var name) ? name : string.Empty; + Email = Claims.TryGetValue("email", out var email) ? email : string.Empty; + } + + internal static IIdentityContext Empty => new IdentityContext(); + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Decorators/OutboxCommandHandlerDecorator.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Decorators/OutboxCommandHandlerDecorator.cs new file mode 100644 index 00000000..704245c7 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Decorators/OutboxCommandHandlerDecorator.cs @@ -0,0 +1,35 @@ +using Convey.CQRS.Commands; +using Convey.MessageBrokers; +using Convey.MessageBrokers.Outbox; +using Convey.Types; + +namespace MiniSpace.Services.Notifications.Infrastructure.Decorators +{ + [Decorator] + internal sealed class OutboxCommandHandlerDecorator : ICommandHandler + where TCommand : class, ICommand + { + private readonly ICommandHandler _handler; + private readonly IMessageOutbox _outbox; + private readonly string _messageId; + private readonly bool _enabled; + + public OutboxCommandHandlerDecorator(ICommandHandler handler, IMessageOutbox outbox, + OutboxOptions outboxOptions, IMessagePropertiesAccessor messagePropertiesAccessor) + { + _handler = handler; + _outbox = outbox; + _enabled = outboxOptions.Enabled; + + var messageProperties = messagePropertiesAccessor.MessageProperties; + _messageId = string.IsNullOrWhiteSpace(messageProperties?.MessageId) + ? Guid.NewGuid().ToString("N") + : messageProperties.MessageId; + } + + public Task HandleAsync(TCommand command, CancellationToken cancellationToken) + => _enabled + ? _outbox.HandleAsync(_messageId, () => _handler.HandleAsync(command)) + : _handler.HandleAsync(command); + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Decorators/OutboxEventHandlerDecorator.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Decorators/OutboxEventHandlerDecorator.cs new file mode 100644 index 00000000..35d50783 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Decorators/OutboxEventHandlerDecorator.cs @@ -0,0 +1,35 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; +using Convey.MessageBrokers.Outbox; +using Convey.Types; + +namespace MiniSpace.Services.Notifications.Infrastructure.Decorators +{ + [Decorator] + internal sealed class OutboxEventHandlerDecorator : IEventHandler + where TEvent : class, IEvent + { + private readonly IEventHandler _handler; + private readonly IMessageOutbox _outbox; + private readonly string _messageId; + private readonly bool _enabled; + + public OutboxEventHandlerDecorator(IEventHandler handler, IMessageOutbox outbox, + OutboxOptions outboxOptions, IMessagePropertiesAccessor messagePropertiesAccessor) + { + _handler = handler; + _outbox = outbox; + _enabled = outboxOptions.Enabled; + + var messageProperties = messagePropertiesAccessor.MessageProperties; + _messageId = string.IsNullOrWhiteSpace(messageProperties?.MessageId) + ? Guid.NewGuid().ToString("N") + : messageProperties.MessageId; + } + + public Task HandleAsync(TEvent @event, CancellationToken cancellationToken) + => _enabled + ? _outbox.HandleAsync(_messageId, () => _handler.HandleAsync(@event)) + : _handler.HandleAsync(@event); + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Exceptions/ExceptionToMessageMapper.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Exceptions/ExceptionToMessageMapper.cs new file mode 100644 index 00000000..e191642f --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Exceptions/ExceptionToMessageMapper.cs @@ -0,0 +1,38 @@ +using Convey.MessageBrokers.RabbitMQ; +using MiniSpace.Services.Notifications.Application.Commands; +using MiniSpace.Services.Notifications.Application.Events.Rejected; +using MiniSpace.Services.Notifications.Application.Exceptions; + +namespace MiniSpace.Services.Notifications.Infrastructure.Exceptions +{ + internal sealed class ExceptionToMessageMapper : IExceptionToMessageMapper + { + public object Map(Exception exception, object message) + => exception switch + { + NotificationNotFoundException ex => message switch + { + DeleteNotification command => new NotificationDeletionRejected(command.NotificationId, "Notification not found", ex.Code), + UpdateNotificationStatus command => new NotificationUpdateRejected(command.NotificationId, "Notification not found", ex.Code), + CreateNotification command => new NotificationCreationRejected(command.NotificationId, "Notification not found", ex.Code), + _ => new NotificationProcessRejected(ex.NotificationId, ex.Message, ex.Code), + }, + InvalidNotificationStatusException ex => message switch + { + UpdateNotificationStatus command => new NotificationUpdateRejected(command.NotificationId, ex.Message, ex.Code), + _ => new NotificationProcessRejected(Guid.Empty, ex.Message, ex.Code), + }, + NotificationAlreadyDeletedException ex => message switch + { + DeleteNotification command => new NotificationDeletionRejected(command.NotificationId, ex.Message, ex.Code), + UpdateNotificationStatus command => new NotificationUpdateRejected(command.NotificationId, ex.Message, ex.Code), + _ => new NotificationProcessRejected(ex.NotificationId, ex.Message, ex.Code), + }, + AppException ex => message switch + { + _ => new NotificationProcessRejected(Guid.Empty, ex.Message, ex.Code) + }, + _ => null + }; + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Exceptions/ExceptionToResponseMapper.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Exceptions/ExceptionToResponseMapper.cs new file mode 100644 index 00000000..cf7aeda2 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Exceptions/ExceptionToResponseMapper.cs @@ -0,0 +1,46 @@ +using System.Collections.Concurrent; +using System.Net; +using Convey; +using Convey.WebApi.Exceptions; +using MiniSpace.Services.Notifications.Application.Exceptions; +using MiniSpace.Services.Notifications.Core.Exceptions; + +namespace MiniSpace.Services.Notifications.Infrastructure.Exceptions +{ + internal sealed class ExceptionToResponseMapper : IExceptionToResponseMapper + { + private static readonly ConcurrentDictionary Codes = new ConcurrentDictionary(); + + public ExceptionResponse Map(Exception exception) + => exception switch + { + DomainException ex => new ExceptionResponse(new {code = GetCode(ex), reason = ex.Message}, + HttpStatusCode.BadRequest), + AppException ex => new ExceptionResponse(new {code = GetCode(ex), reason = ex.Message}, + HttpStatusCode.BadRequest), + _ => new ExceptionResponse(new {code = "error", reason = "There was an error."}, + HttpStatusCode.BadRequest) + }; + + private static string GetCode(Exception exception) + { + var type = exception.GetType(); + if (Codes.TryGetValue(type, out var code)) + { + return code; + } + + var exceptionCode = exception switch + { + DomainException domainException when !string.IsNullOrWhiteSpace(domainException.Code) => domainException + .Code, + AppException appException when !string.IsNullOrWhiteSpace(appException.Code) => appException.Code, + _ => exception.GetType().Name.Underscore().Replace("_exception", string.Empty) + }; + + Codes.TryAdd(type, exceptionCode); + + return exceptionCode; + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Extensions.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Extensions.cs new file mode 100644 index 00000000..cff565b7 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Extensions.cs @@ -0,0 +1,158 @@ +using System.Text; +using Convey; +using Convey.CQRS.Commands; +using Convey.CQRS.Events; +using Convey.CQRS.Queries; +using Convey.Discovery.Consul; +using Convey.Docs.Swagger; +using Convey.HTTP; +using Convey.LoadBalancing.Fabio; +using Convey.MessageBrokers; +using Convey.MessageBrokers.CQRS; +using Convey.MessageBrokers.Outbox; +using Convey.MessageBrokers.Outbox.Mongo; +using Convey.MessageBrokers.RabbitMQ; +using Convey.Metrics.AppMetrics; +using Convey.Persistence.MongoDB; +using Convey.Persistence.Redis; +using Convey.Security; +using Convey.Tracing.Jaeger; +using Convey.Tracing.Jaeger.RabbitMQ; +using Convey.WebApi; +using Convey.WebApi.CQRS; +using Convey.WebApi.Security; +using Convey.WebApi.Swagger; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; +using MiniSpace.Services.Notifications.Application; +using MiniSpace.Services.Notifications.Application.Commands; +using MiniSpace.Services.Notifications.Application.Events.External; +using MiniSpace.Services.Notifications.Application.Events.External.Handlers; +using MiniSpace.Services.Notifications.Application.Services; +using MiniSpace.Services.Notifications.Core.Repositories; +using MiniSpace.Services.Notifications.Infrastructure.Contexts; +using MiniSpace.Services.Notifications.Infrastructure.Decorators; +using MiniSpace.Services.Notifications.Infrastructure.Exceptions; +using MiniSpace.Services.Notifications.Infrastructure.Logging; +using MiniSpace.Services.Notifications.Infrastructure.Mongo.Documents; +using MiniSpace.Services.Notifications.Infrastructure.Mongo.Repositories; +using MiniSpace.Services.Notifications.Infrastructure.Services; +using MiniSpace.Services.Notifications.Infrastructure; +using MiniSpace.Services.Notifications.Application.Services.Clients; +using MiniSpace.Services.Notifications.Infrastructure.Services.Clients; + +namespace MiniSpace.Services.Notifications.Infrastructure +{ + public static class Extensions + { + public static IConveyBuilder AddInfrastructure(this IConveyBuilder builder) + { + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(ctx => ctx.GetRequiredService().Create()); + builder.Services.TryDecorate(typeof(ICommandHandler<>), typeof(OutboxCommandHandlerDecorator<>)); + builder.Services.TryDecorate(typeof(IEventHandler<>), typeof(OutboxEventHandlerDecorator<>)); + builder.Services.AddHostedService(); + + return builder + .AddErrorHandler() + .AddQueryHandlers() + .AddInMemoryQueryDispatcher() + .AddHttpClient() + .AddConsul() + .AddFabio() + .AddRabbitMq(plugins: p => p.AddJaegerRabbitMqPlugin()) + .AddMessageOutbox(o => o.AddMongo()) + .AddExceptionToMessageMapper() + .AddMongo() + .AddRedis() + .AddMetrics() + .AddJaeger() + .AddHandlersLogging() + .AddMongoRepository("notifications") + .AddMongoRepository("friend-service") + .AddMongoRepository("students") + .AddMongoRepository("students-notifications") + // .AddMongoRepository("events-service") + .AddWebApiSwaggerDocs() + .AddCertificateAuthentication() + .AddSecurity(); + } + + public static IApplicationBuilder UseInfrastructure(this IApplicationBuilder app) + { + app.UseErrorHandler() + .UseSwaggerDocs() + .UseJaeger() + .UseConvey() + .UsePublicContracts() + .UseMetrics() + .UseCertificateAuthentication() + .UseRabbitMq() + .SubscribeCommand() + .SubscribeCommand() + .SubscribeCommand() + // .SubscribeEvent() + // .SubscribeEvent() + .SubscribeEvent() + .SubscribeEvent() + .SubscribeEvent() + .SubscribeEvent() + .SubscribeEvent() + .SubscribeEvent(); + // .SubscribeEvent() + // .SubscribeEvent() + // .SubscribeEvent(); + + return app; + } + + internal static CorrelationContext GetCorrelationContext(this IHttpContextAccessor accessor) + => accessor.HttpContext?.Request.Headers.TryGetValue("Correlation-Context", out var json) is true + ? JsonConvert.DeserializeObject(json.FirstOrDefault()) + : null; + + internal static IDictionary GetHeadersToForward(this IMessageProperties messageProperties) + { + const string sagaHeader = "Saga"; + if (messageProperties?.Headers is null || !messageProperties.Headers.TryGetValue(sagaHeader, out var saga)) + { + return null; + } + + return saga is null + ? null + : new Dictionary + { + [sagaHeader] = saga + }; + } + + internal static string GetSpanContext(this IMessageProperties messageProperties, string header) + { + if (messageProperties is null) + { + return string.Empty; + } + + if (messageProperties.Headers.TryGetValue(header, out var span) && span is byte[] spanBytes) + { + return Encoding.UTF8.GetString(spanBytes); + } + + return string.Empty; + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/IAppContextFactory.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/IAppContextFactory.cs new file mode 100644 index 00000000..717fd557 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/IAppContextFactory.cs @@ -0,0 +1,9 @@ +using MiniSpace.Services.Notifications.Application; + +namespace MiniSpace.Services.Notifications.Infrastructure +{ + public interface IAppContextFactory + { + IAppContext Create(); + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/IExtendedStudentNotificationsRepository.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/IExtendedStudentNotificationsRepository.cs new file mode 100644 index 00000000..f6f4dce7 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/IExtendedStudentNotificationsRepository.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; +using MiniSpace.Services.Notifications.Core.Repositories; +using MiniSpace.Services.Notifications.Infrastructure.Mongo.Documents; +using MongoDB.Driver; + +namespace MiniSpace.Services.Notifications.Infrastructure.Mongo.Repositories +{ + public interface IExtendedStudentNotificationsRepository : IStudentNotificationsRepository + { + Task BulkUpdateAsync(FilterDefinition filter, UpdateDefinition update); + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Logging/Extensions.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Logging/Extensions.cs new file mode 100644 index 00000000..18ceacc0 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Logging/Extensions.cs @@ -0,0 +1,21 @@ +using Convey; +using Convey.Logging.CQRS; +using Microsoft.Extensions.DependencyInjection; +using MiniSpace.Services.Notifications.Application.Commands; + +namespace MiniSpace.Services.Notifications.Infrastructure.Logging +{ + internal static class Extensions + { + public static IConveyBuilder AddHandlersLogging(this IConveyBuilder builder) + { + var assembly = typeof(UpdateNotificationStatus).Assembly; + + builder.Services.AddSingleton(new MessageToLogTemplateMapper()); + + return builder + .AddCommandHandlersLogging(assembly) + .AddEventHandlersLogging(assembly); + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Logging/MessageToLogTemplateMapper.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Logging/MessageToLogTemplateMapper.cs new file mode 100644 index 00000000..3b1911b0 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Logging/MessageToLogTemplateMapper.cs @@ -0,0 +1,66 @@ +using Convey.Logging.CQRS; +using MiniSpace.Services.Notifications.Application.Commands; +using MiniSpace.Services.Notifications.Application.Events.External; + +namespace MiniSpace.Services.Notifications.Infrastructure.Logging +{ + internal sealed class MessageToLogTemplateMapper : IMessageToLogTemplateMapper + { + private static IReadOnlyDictionary MessageTemplates + => new Dictionary + { + { + typeof(CreateNotification), new HandlerLogTemplate + { + After = "Created notification with id: {NotificationId} for user: {UserId}." + } + }, + { + typeof(DeleteNotification), new HandlerLogTemplate + { + After = "Deleted notification with id: {NotificationId}." + } + }, + { + typeof(UpdateNotificationStatus), new HandlerLogTemplate + { + After = "Updated the status of notification with id: {NotificationId} to: {NewStatus}." + } + }, + {typeof(FriendRequestCreated), new HandlerLogTemplate + { + After = "Processed creation of friend request from {RequesterId} to {FriendId}." + } + }, + {typeof(FriendRequestSent), new HandlerLogTemplate + { + After = "Processed friend request sent from {InviterId} to {InviteeId}." + } + }, + {typeof(FriendInvited), new HandlerLogTemplate + { + After = "Handled invitation sent by {InviterId} to {InviteeId}." + } + }, + {typeof(NotificationCreated), new HandlerLogTemplate + { + After = "Notification created with ID: {NotificationId} for user: {UserId}, message: '{Message}' at {CreatedAt}." + } + }, + }; + + public HandlerLogTemplate Map(TMessage message) where TMessage : class + { + var key = message.GetType(); + if (MessageTemplates.TryGetValue(key, out var template)) + { + if (template.After.Contains("{NewStatus}")) + { + template.After = template.After.Replace("{NewStatus}", "{Status}"); + } + return template; + } + return null; + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/MiniSpace.Services.Notifications.Infrastructure.csproj b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/MiniSpace.Services.Notifications.Infrastructure.csproj new file mode 100644 index 00000000..7309d81d --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/MiniSpace.Services.Notifications.Infrastructure.csproj @@ -0,0 +1,39 @@ + + + + net8.0 + enable + disable + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/MiniSpace.Services.Notifications.Infrastructure.sln b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/MiniSpace.Services.Notifications.Infrastructure.sln new file mode 100644 index 00000000..a5c81a95 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/MiniSpace.Services.Notifications.Infrastructure.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniSpace.Services.Notifications.Infrastructure", "MiniSpace.Services.Notifications.Infrastructure.csproj", "{994B662D-5C21-4077-8970-C61A227B46D4}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {994B662D-5C21-4077-8970-C61A227B46D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {994B662D-5C21-4077-8970-C61A227B46D4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {994B662D-5C21-4077-8970-C61A227B46D4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {994B662D-5C21-4077-8970-C61A227B46D4}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {ADCE09CD-5371-42F5-9AB2-04FCDA89D8EA} + EndGlobalSection +EndGlobal diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Mongo/Documents/Extensions.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Mongo/Documents/Extensions.cs new file mode 100644 index 00000000..77c14d4a --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Mongo/Documents/Extensions.cs @@ -0,0 +1,200 @@ +using MiniSpace.Services.Notifications.Application.Dto; +using MiniSpace.Services.Notifications.Core.Entities; +using System; + +namespace MiniSpace.Services.Notifications.Infrastructure.Mongo.Documents +{ + public static class Extensions + { + public static Notification AsEntity(this NotificationDocument document) + => new Notification( + document.NotificationId, + document.UserId, + document.Message, + Enum.Parse(document.Status, true), + document.CreatedAt, + document.UpdatedAt, + document.EventType, + document.RelatedEntityId); + + public static NotificationDocument AsDocument(this Notification entity) + => new NotificationDocument + { + NotificationId = entity.NotificationId, + UserId = entity.UserId, + Message = entity.Message, + Status = entity.Status.ToString(), + CreatedAt = entity.CreatedAt, + UpdatedAt = entity.UpdatedAt, + RelatedEntityId = entity.RelatedEntityId, + EventType = entity.EventType + }; + + public static NotificationDto AsDto(this Notification entity) + => new NotificationDto + { + NotificationId = entity.NotificationId, + UserId = entity.UserId, + Message = entity.Message, + Status = entity.Status.ToString(), + CreatedAt = entity.CreatedAt, + UpdatedAt = entity.UpdatedAt, + RelatedEntityId = entity.RelatedEntityId, + EventType = entity.EventType + }; + + public static NotificationDto AsDto(this NotificationDocument document) + => new NotificationDto + { + NotificationId = document.NotificationId, + UserId = document.UserId, + Message = document.Message, + Status = document.Status, + CreatedAt = document.CreatedAt, + UpdatedAt = document.UpdatedAt, + RelatedEntityId = document.RelatedEntityId, + EventType = document.EventType + }; + + + public static FriendEvent AsEntity(this FriendEventDocument document) + { + return new FriendEvent( + document.Id, + document.EventId, + document.UserId, + document.FriendId, + document.EventType, + document.Details, + document.CreatedAt + ); + } + + public static FriendEventDocument AsDocument(this FriendEvent entity) + { + return new FriendEventDocument + { + Id = entity.Id, + EventId = entity.EventId, + UserId = entity.UserId, + FriendId = entity.FriendId, + EventType = entity.EventType, + Details = entity.Details, + CreatedAt = entity.CreatedAt + }; + } + + public static FriendEventDto AsDto(this FriendEventDocument document) + { + return new FriendEventDto + { + Id = document.Id, + EventId = document.EventId, + UserId = document.UserId, + EventType = document.EventType, + Details = document.Details, + CreatedAt = document.CreatedAt + }; + } + + public static Student AsEntity(this StudentDocument document) + { + return new Student( + document.Id, + document.Email, + document.FirstName, + document.LastName, + document.NumberOfFriends, + document.ProfileImage, + document.Description, + document.DateOfBirth, + document.EmailNotifications, + document.IsBanned, + document.IsOrganizer, + document.State, + document.CreatedAt + ); + } + + public static StudentDocument AsDocument(this Student entity) + { + return new StudentDocument + { + Id = entity.Id, + Email = entity.Email, + FirstName = entity.FirstName, + LastName = entity.LastName, + NumberOfFriends = entity.NumberOfFriends, + ProfileImage = entity.ProfileImage, + Description = entity.Description, + DateOfBirth = entity.DateOfBirth, + EmailNotifications = entity.EmailNotifications, + IsBanned = entity.IsBanned, + IsOrganizer = entity.IsOrganizer, + State = entity.State, + CreatedAt = entity.CreatedAt, + InterestedInEvents = entity.InterestedInEvents, + SignedUpEvents = entity.SignedUpEvents + }; + } + + public static StudentDto AsDto(this StudentDocument document) + { + return new StudentDto + { + Id = document.Id, + Email = document.Email, + FirstName = document.FirstName, + LastName = document.LastName, + NumberOfFriends = document.NumberOfFriends, + ProfileImage = document.ProfileImage, + Description = document.Description, + DateOfBirth = document.DateOfBirth, + EmailNotifications = document.EmailNotifications, + IsBanned = document.IsBanned, + IsOrganizer = document.IsOrganizer, + State = document.State, + CreatedAt = document.CreatedAt, + InterestedInEvents = document.InterestedInEvents, + SignedUpEvents = document.SignedUpEvents + }; + } + + public static StudentNotifications AsEntity(this StudentNotificationsDocument document) + { + var studentNotifications = new StudentNotifications(document.StudentId); + foreach (var notificationDocument in document.Notifications) + { + var notification = notificationDocument.AsEntity(); + studentNotifications.AddNotification(notification); + } + return studentNotifications; + } + + public static StudentNotificationsDocument AsDocument(this StudentNotifications entity) + { + var notifications = new List(); + foreach (var notification in entity.Notifications) + { + notifications.Add(notification.AsDocument()); + } + + return new StudentNotificationsDocument + { + Id = Guid.NewGuid(), + StudentId = entity.StudentId, + Notifications = notifications + }; + } + + public static IEnumerable AsDto(this StudentNotificationsDocument document) + { + return document.Notifications.Select(nd => nd.AsDto()); + } + + public static List AsDocumentList(this IEnumerable notifications) + { + return notifications.Select(n => n.AsDocument()).ToList(); + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Mongo/Documents/FriendEventDocument.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Mongo/Documents/FriendEventDocument.cs new file mode 100644 index 00000000..befdb7b8 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Mongo/Documents/FriendEventDocument.cs @@ -0,0 +1,18 @@ +using Convey.Types; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using System; + +namespace MiniSpace.Services.Notifications.Infrastructure.Mongo.Documents +{ + public class FriendEventDocument : IIdentifiable + { + public Guid Id { get; set; } + public Guid EventId { get; set; } + public Guid UserId { get; set; } + public Guid FriendId { get; set; } + public string EventType { get; set; } + public string Details { get; set; } + public DateTime CreatedAt { get; set; } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Mongo/Documents/NotificationDocument.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Mongo/Documents/NotificationDocument.cs new file mode 100644 index 00000000..83ec326a --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Mongo/Documents/NotificationDocument.cs @@ -0,0 +1,22 @@ +using Convey.Types; +using MiniSpace.Services.Notifications.Core.Entities; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using System; + +namespace MiniSpace.Services.Notifications.Infrastructure.Mongo.Documents +{ + public class NotificationDocument : IIdentifiable + { + public Guid Id { get; set; } + public Guid NotificationId { get; set; } + public Guid UserId { get; set; } + public string Message { get; set; } + public string Status { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime? UpdatedAt { get; set; } + public Guid? RelatedEntityId { get; set; } + public NotificationEventType EventType { get; set; } + + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Mongo/Documents/StudentDocument.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Mongo/Documents/StudentDocument.cs new file mode 100644 index 00000000..654dc063 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Mongo/Documents/StudentDocument.cs @@ -0,0 +1,24 @@ +using System; +using Convey.Types; + +namespace MiniSpace.Services.Notifications.Infrastructure.Mongo.Documents +{ + public class StudentDocument : IIdentifiable + { + public Guid Id { get; set; } + public string Email { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public int NumberOfFriends { get; set; } + public string ProfileImage { get; set; } + public string Description { get; set; } + public DateTime DateOfBirth { get; set; } + public bool EmailNotifications { get; set; } + public bool IsBanned { get; set; } + public bool IsOrganizer { get; set; } + public string State { get; set; } + public DateTime CreatedAt { get; set; } + public List InterestedInEvents { get; set; } + public List SignedUpEvents { get; set; } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Mongo/Documents/StudentNotificationsDocument.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Mongo/Documents/StudentNotificationsDocument.cs new file mode 100644 index 00000000..5572bfa8 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Mongo/Documents/StudentNotificationsDocument.cs @@ -0,0 +1,19 @@ +using Convey.Types; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Notifications.Infrastructure.Mongo.Documents +{ + public class StudentNotificationsDocument : IIdentifiable + { + [BsonId] + [BsonRepresentation(BsonType.String)] + public Guid Id { get; set; } + + [BsonRepresentation(BsonType.String)] + public Guid StudentId { get; set; } + public List Notifications { get; set; } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Mongo/Queries/Handlers/GetNotificationHandler.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Mongo/Queries/Handlers/GetNotificationHandler.cs new file mode 100644 index 00000000..f23ecac7 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Mongo/Queries/Handlers/GetNotificationHandler.cs @@ -0,0 +1,41 @@ +using Convey.CQRS.Queries; +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Notifications.Application.Dto; +using MiniSpace.Services.Notifications.Application.Exceptions; +using MiniSpace.Services.Notifications.Application.Queries; +using MiniSpace.Services.Notifications.Infrastructure.Mongo.Documents; +using MongoDB.Driver; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Notifications.Infrastructure.Mongo.Queries.Handlers +{ + public class GetNotificationHandler : IQueryHandler + { + private readonly IMongoRepository _repository; + + public GetNotificationHandler(IMongoRepository repository) + { + _repository = repository; + } + + public async Task HandleAsync(GetNotification query, CancellationToken cancellationToken) + { + var document = await _repository.GetAsync(d => d.StudentId == query.UserId); + + if (document == null) + { + throw new NotificationNotFoundException(query.UserId, $"User with ID {query.UserId} not found."); + } + + var notification = document.Notifications.FirstOrDefault(n => n.NotificationId == query.NotificationId); + if (notification == null) + { + throw new NotificationNotFoundException(query.NotificationId); + } + + return notification.AsDto(); + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Mongo/Queries/Handlers/GetNotificationsByUserHandler.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Mongo/Queries/Handlers/GetNotificationsByUserHandler.cs new file mode 100644 index 00000000..b6e7622c --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Mongo/Queries/Handlers/GetNotificationsByUserHandler.cs @@ -0,0 +1,73 @@ +using Convey.CQRS.Queries; +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Notifications.Application.Dto; +using MiniSpace.Services.Notifications.Application.Queries; +using MiniSpace.Services.Notifications.Infrastructure.Mongo.Documents; +using MongoDB.Driver; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Notifications.Infrastructure.Mongo.Queries.Handlers +{ + public class GetNotificationsByUserHandler : IQueryHandler> + { + private readonly IMongoRepository _repository; + private const string BaseUrl = "notifications"; + + public GetNotificationsByUserHandler(IMongoRepository repository) + { + _repository = repository; + } + + public async Task> HandleAsync(GetNotificationsByUser query, CancellationToken cancellationToken) + { + var filter = Builders.Filter.Eq(doc => doc.StudentId, query.UserId); + + if (!string.IsNullOrEmpty(query.Status)) + { + var regex = new MongoDB.Bson.BsonRegularExpression($"^{Regex.Escape(query.Status)}$", "i"); + var statusFilter = Builders.Filter.ElemMatch(x => x.Notifications, + Builders.Filter.Regex("Status", regex)); + + filter = Builders.Filter.And(filter, statusFilter); + + Console.WriteLine($"Applying status filter with regex: {regex}"); + } + + var sortBy = Builders.Sort.Descending(doc => doc.Notifications[-1].CreatedAt); + + if (query.SortOrder.ToLower() == "asc") + { + sortBy = Builders.Sort.Ascending(doc => doc.Notifications[-1].CreatedAt); + } + + var notificationsList = new List(); + + var documents = await _repository.Collection.Find(filter).ToListAsync(cancellationToken); + + var allNotifications = documents.SelectMany(doc => doc.Notifications + .Where(n => (!query.StartDate.HasValue || n.CreatedAt >= query.StartDate.Value) && + (!query.EndDate.HasValue || n.CreatedAt <= query.EndDate.Value) && + (string.IsNullOrEmpty(query.Status) || n.Status.Equals(query.Status, StringComparison.OrdinalIgnoreCase))) + .Select(n => n.AsDto())) + .OrderByDescending(n => n.CreatedAt) + .ToList(); + + if (query.SortOrder.ToLower() == "asc") + { + allNotifications = allNotifications.OrderBy(n => n.CreatedAt).ToList(); + } + + var totalNotifications = allNotifications.Count; + var paginatedNotifications = allNotifications + .Skip((query.Page - 1) * query.ResultsPerPage) + .Take(query.ResultsPerPage) + .ToList(); + + return new Application.Queries.PagedResult(paginatedNotifications, totalNotifications, query.ResultsPerPage, query.Page, BaseUrl); + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Mongo/Repositories/FriendEventMongoRepository.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Mongo/Repositories/FriendEventMongoRepository.cs new file mode 100644 index 00000000..efa65c15 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Mongo/Repositories/FriendEventMongoRepository.cs @@ -0,0 +1,49 @@ +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Notifications.Infrastructure.Mongo.Documents; +using MiniSpace.Services.Notifications.Core.Repositories; +using MongoDB.Driver; +using System; +using System.Threading.Tasks; +using MiniSpace.Services.Notifications.Core.Entities; + +namespace MiniSpace.Services.Notifications.Infrastructure.Mongo.Repositories +{ + public class FriendEventMongoRepository : IFriendEventRepository + { + private readonly IMongoRepository _repository; + + public FriendEventMongoRepository(IMongoRepository repository) + { + _repository = repository; + } + + public async Task AddAsync(FriendEvent friendEvent) + { + var document = friendEvent.AsDocument(); + await _repository.AddAsync(document); + } + + public async Task GetAsync(Guid id) + { + var document = await _repository.GetAsync(id); + return document?.AsEntity(); + } + + public async Task UpdateAsync(FriendEvent friendEvent) + { + var document = friendEvent.AsDocument(); + var filter = Builders.Filter.Eq(doc => doc.Id, document.Id); + var update = Builders.Update + .Set(doc => doc.EventType, document.EventType) + .Set(doc => doc.Details, document.Details) + .Set(doc => doc.CreatedAt, DateTime.UtcNow); + + await _repository.Collection.UpdateOneAsync(filter, update); + } + + public Task DeleteAsync(Guid id) + { + return _repository.DeleteAsync(id); + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Mongo/Repositories/NotificationMongoRepository.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Mongo/Repositories/NotificationMongoRepository.cs new file mode 100644 index 00000000..956c6ba9 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Mongo/Repositories/NotificationMongoRepository.cs @@ -0,0 +1,42 @@ +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Notifications.Core.Entities; +using MiniSpace.Services.Notifications.Core.Repositories; +using MiniSpace.Services.Notifications.Infrastructure.Mongo.Documents; +using MongoDB.Driver; +using System; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Notifications.Infrastructure.Mongo.Repositories +{ + public class NotificationMongoRepository : INotificationRepository + { + private readonly IMongoRepository _repository; + + public NotificationMongoRepository(IMongoRepository repository) + { + _repository = repository; + } + + public async Task GetAsync(Guid id) + { + var document = await _repository.GetAsync(o => o.NotificationId == id); + return document?.AsEntity(); + } + + public Task AddAsync(Notification notification) + => _repository.AddAsync(notification.AsDocument()); + + public async Task UpdateAsync(Notification notification) + { + var filter = Builders.Filter.Eq(doc => doc.NotificationId, notification.NotificationId); + var update = Builders.Update + .Set(doc => doc.Status, notification.Status.ToString()) + .Set(doc => doc.UpdatedAt, DateTime.UtcNow); + + var updateResult = await _repository.Collection.UpdateOneAsync(filter, update); + } + + public Task DeleteAsync(Guid id) + => _repository.DeleteAsync(id); + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Mongo/Repositories/StudentMongoRepository.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Mongo/Repositories/StudentMongoRepository.cs new file mode 100644 index 00000000..9cdcf95e --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Mongo/Repositories/StudentMongoRepository.cs @@ -0,0 +1,61 @@ +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Notifications.Core.Entities; +using MiniSpace.Services.Notifications.Core.Repositories; +using MiniSpace.Services.Notifications.Infrastructure.Mongo.Documents; +using MongoDB.Driver; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Notifications.Infrastructure.Mongo.Repositories +{ + public class StudentMongoRepository : IStudentRepository + { + private readonly IMongoRepository _repository; + + public StudentMongoRepository(IMongoRepository repository) + { + _repository = repository; + } + + public async Task GetAsync(Guid id) + { + var document = await _repository.GetAsync(o => o.Id == id); + return document?.AsEntity(); + } + + public async Task> GetAllAsync() + { + var documents = await _repository.FindAsync(_ => true); + List students = new List(); + foreach (var doc in documents) + { + students.Add(doc.AsEntity()); + } + return students; + } + + public Task AddAsync(Student student) + => _repository.AddAsync(student.AsDocument()); + + public async Task UpdateAsync(Student student) + { + var filter = Builders.Filter.Eq(doc => doc.Id, student.Id); + var update = Builders.Update + .Set(doc => doc.FirstName, student.FirstName) + .Set(doc => doc.LastName, student.LastName) + .Set(doc => doc.ProfileImage, student.ProfileImage) + // Ensure to update other fields as necessary + .Set(doc => doc.Email, student.Email) + .Set(doc => doc.Description, student.Description) + .Set(doc => doc.DateOfBirth, student.DateOfBirth) + .Set(doc => doc.State, student.State); + + await _repository.Collection.UpdateOneAsync(filter, update); + } + + + public Task DeleteAsync(Guid id) + => _repository.DeleteAsync(id); + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Mongo/Repositories/StudentNotificationsMongoRepository.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Mongo/Repositories/StudentNotificationsMongoRepository.cs new file mode 100644 index 00000000..13d97a97 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Mongo/Repositories/StudentNotificationsMongoRepository.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Notifications.Core.Entities; +using MiniSpace.Services.Notifications.Core.Repositories; +using MiniSpace.Services.Notifications.Infrastructure.Mongo.Documents; +using MongoDB.Driver; + +namespace MiniSpace.Services.Notifications.Infrastructure.Mongo.Repositories +{ + public class StudentNotificationsMongoRepository : IExtendedStudentNotificationsRepository + { + private readonly IMongoRepository _repository; + + public StudentNotificationsMongoRepository(IMongoRepository repository) + { + _repository = repository; + } + + public async Task GetByStudentIdAsync(Guid studentId) + { + var document = await _repository.GetAsync(d => d.StudentId == studentId); + return document?.AsEntity(); + } + + public async Task AddAsync(StudentNotifications studentNotifications) + { + var document = studentNotifications.AsDocument(); + await _repository.AddAsync(document); + } + + public async Task UpdateAsync(StudentNotifications studentNotifications) + { + var filter = Builders.Filter.Eq(doc => doc.StudentId, studentNotifications.StudentId); + var update = Builders.Update + .SetOnInsert(doc => doc.Id, studentNotifications.StudentId) + .PushEach(doc => doc.Notifications, studentNotifications.Notifications.Select(n => n.AsDocument())); + + var options = new UpdateOptions { IsUpsert = true }; + await _repository.Collection.UpdateOneAsync(filter, update, options); + } + + public async Task AddOrUpdateAsync(StudentNotifications studentNotifications) + { + var document = studentNotifications.AsDocument(); + var filter = Builders.Filter.Eq(doc => doc.StudentId, studentNotifications.StudentId); + var updates = new List>(); + + foreach (var notification in studentNotifications.Notifications) + { + updates.Add(Builders.Update.Push(doc => doc.Notifications, notification.AsDocument())); + } + + var update = Builders.Update + .SetOnInsert(doc => doc.Id, studentNotifications.StudentId) // Ensures the ID is set on insert + .AddToSetEach(doc => doc.Notifications, studentNotifications.Notifications.Select(n => n.AsDocument())); // Use AddToSetEach to avoid duplicates + + var options = new UpdateOptions { IsUpsert = true }; + await _repository.Collection.UpdateOneAsync(filter, update, options); + } + + + public async Task> FindAsync(FilterDefinition filter, FindOptions options) + { + var documents = await _repository.Collection.Find(filter, options).ToListAsync(); + return documents.Select(doc => doc.AsEntity()).ToList(); + } + + + public Task DeleteAsync(Guid studentId) + { + return _repository.DeleteAsync(studentId); + } + + public async Task BulkUpdateAsync(FilterDefinition filter, UpdateDefinition update) + { + return await _repository.Collection.UpdateManyAsync(filter, update); + } + + public async Task UpdateNotificationStatus(Guid studentId, Guid notificationId, string newStatus) + { + var filter = Builders.Filter.And( + Builders.Filter.Eq(doc => doc.StudentId, studentId), + Builders.Filter.ElemMatch(doc => doc.Notifications, n => n.NotificationId == notificationId) + ); + + var update = Builders.Update + .Set("Notifications.$.Status", newStatus) + .CurrentDate("Notifications.$.UpdatedAt"); + + await _repository.Collection.UpdateOneAsync(filter, update); + } + + public async Task NotificationExists(Guid studentId, Guid notificationId) + { + var filter = Builders.Filter.And( + Builders.Filter.Eq(doc => doc.StudentId, studentId), + Builders.Filter.ElemMatch(doc => doc.Notifications, n => n.NotificationId == notificationId) + ); + + var result = await _repository.Collection.Find(filter).AnyAsync(); + return result; + } + + public async Task DeleteNotification(Guid studentId, Guid notificationId) + { + var filter = Builders.Filter.And( + Builders.Filter.Eq(d => d.StudentId, studentId), + Builders.Filter.ElemMatch(e => e.Notifications, n => n.NotificationId == notificationId)); + + var update = Builders.Update.PullFilter( + p => p.Notifications, n => n.NotificationId == notificationId); + + await _repository.Collection.UpdateOneAsync(filter, update); + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Services/Clients/FriendsServiceClient.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Services/Clients/FriendsServiceClient.cs new file mode 100644 index 00000000..347ea8a7 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Services/Clients/FriendsServiceClient.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Convey.HTTP; +using MiniSpace.Services.Notifications.Application.Dto; +using MiniSpace.Services.Notifications.Application.Services.Clients; + +namespace MiniSpace.Services.Notifications.Infrastructure.Services.Clients +{ + public class FriendsServiceClient : IFriendsServiceClient + { + private readonly IHttpClient _httpClient; + private readonly string _url; + + public FriendsServiceClient(IHttpClient httpClient, HttpClientOptions options) + { + _httpClient = httpClient; + _url = options.Services["friends"]; + } + + public Task> GetAsync(Guid studentId) + => _httpClient.GetAsync>($"{_url}/friends/{studentId}"); + public Task> GetFriendsAsync(Guid studentId) + => _httpClient.GetAsync>($"{_url}/friends/{studentId}"); + public Task> GetRequestsAsync(Guid studentId) + => _httpClient.GetAsync>($"{_url}/friends/requests/{studentId}"); + + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Services/Clients/StudentsServiceClient.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Services/Clients/StudentsServiceClient.cs new file mode 100644 index 00000000..1fb4cc5f --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Services/Clients/StudentsServiceClient.cs @@ -0,0 +1,27 @@ +using System; +using System.Threading.Tasks; +using Convey.HTTP; +using MiniSpace.Services.Notifications.Application.Dto; +using MiniSpace.Services.Notifications.Application.Services.Clients; + +namespace MiniSpace.Services.Notifications.Infrastructure.Services.Clients +{ + public class StudentsServiceClient : IStudentsServiceClient + { + private readonly IHttpClient _httpClient; + private readonly string _url; + + public StudentsServiceClient(IHttpClient httpClient, HttpClientOptions options) + { + _httpClient = httpClient; + _url = options.Services["students"]; + } + + public Task GetAsync(Guid id) + => _httpClient.GetAsync($"{_url}/students/{id}"); + + public Task> GetAllAsync() + => _httpClient.GetAsync>($"{_url}/students"); + + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Services/DateTimeProvider.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Services/DateTimeProvider.cs new file mode 100644 index 00000000..715bdb45 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Services/DateTimeProvider.cs @@ -0,0 +1,9 @@ +using MiniSpace.Services.Notifications.Application.Services; + +namespace MiniSpace.Services.Notifications.Infrastructure.Services +{ + internal sealed class DateTimeProvider : IDateTimeProvider + { + public DateTime Now => DateTime.UtcNow; + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Services/EventMapper.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Services/EventMapper.cs new file mode 100644 index 00000000..48e03bb0 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Services/EventMapper.cs @@ -0,0 +1,31 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Notifications.Application.Services; +using MiniSpace.Services.Notifications.Core; +using MiniSpace.Services.Notifications.Core.Events; + +namespace MiniSpace.Services.Notifications.Infrastructure.Services +{ + public class EventMapper : IEventMapper + { + public IEnumerable MapAll(IEnumerable events) + => events.Select(Map); + + public IEvent Map(IDomainEvent @event) + { + switch (@event) + { + case NotificationCreated e: + return new MiniSpace.Services.Notifications.Application.Events.External.NotificationCreated( + e.NotificationId, e.UserId, e.Message, e.CreatedAt); + case NotificationUpdated e: + return new MiniSpace.Services.Notifications.Application.Events.External.NotificationUpdated( + e.NotificationId, e.UserId, e.NewStatus); + case NotificationDeleted e: + return new MiniSpace.Services.Notifications.Application.Events.External.NotificationDeleted( + e.UserId, e.NotificationId); + } + + return null; + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Services/MessageBroker.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Services/MessageBroker.cs new file mode 100644 index 00000000..4be1aef9 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Services/MessageBroker.cs @@ -0,0 +1,85 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; +using Convey.MessageBrokers.Outbox; +using Convey.MessageBrokers.RabbitMQ; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using OpenTracing; +using MiniSpace.Services.Notifications.Application.Services; +using MiniSpace.Services.Notifications.Infrastructure; + +namespace MiniSpace.Services.Notifications.Infrastructure.Services +{ + internal sealed class MessageBroker : IMessageBroker + { + private const string DefaultSpanContextHeader = "span_context"; + private readonly IBusPublisher _busPublisher; + private readonly IMessageOutbox _outbox; + private readonly ICorrelationContextAccessor _contextAccessor; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IMessagePropertiesAccessor _messagePropertiesAccessor; + private readonly ITracer _tracer; + private readonly ILogger _logger; + private readonly string _spanContextHeader; + + public MessageBroker(IBusPublisher busPublisher, IMessageOutbox outbox, + ICorrelationContextAccessor contextAccessor, IHttpContextAccessor httpContextAccessor, + IMessagePropertiesAccessor messagePropertiesAccessor, RabbitMqOptions options, ITracer tracer, + ILogger logger) + { + _busPublisher = busPublisher; + _outbox = outbox; + _contextAccessor = contextAccessor; + _httpContextAccessor = httpContextAccessor; + _messagePropertiesAccessor = messagePropertiesAccessor; + _tracer = tracer; + _logger = logger; + _spanContextHeader = string.IsNullOrWhiteSpace(options.SpanContextHeader) + ? DefaultSpanContextHeader + : options.SpanContextHeader; + } + + public Task PublishAsync(params IEvent[] events) => PublishAsync(events?.AsEnumerable()); + + public async Task PublishAsync(IEnumerable events) + { + if (events is null) + { + return; + } + + var messageProperties = _messagePropertiesAccessor.MessageProperties; + var originatedMessageId = messageProperties?.MessageId; + var correlationId = messageProperties?.CorrelationId; + var spanContext = messageProperties?.GetSpanContext(_spanContextHeader); + if (string.IsNullOrWhiteSpace(spanContext)) + { + spanContext = _tracer.ActiveSpan is null ? string.Empty : _tracer.ActiveSpan.Context.ToString(); + } + + var headers = messageProperties.GetHeadersToForward(); + var correlationContext = _contextAccessor.CorrelationContext ?? + _httpContextAccessor.GetCorrelationContext(); + + foreach (var @event in events) + { + if (@event is null) + { + continue; + } + + var messageId = Guid.NewGuid().ToString("N"); + _logger.LogTrace($"Publishing integration event: {@event.GetType().Name} [id: '{messageId}']."); + if (_outbox.Enabled) + { + await _outbox.SendAsync(@event, originatedMessageId, messageId, correlationId, spanContext, + correlationContext, headers); + continue; + } + + await _busPublisher.PublishAsync(@event, messageId, correlationId, spanContext, correlationContext, + headers); + } + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Services/NotificationCleanupService.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Services/NotificationCleanupService.cs new file mode 100644 index 00000000..ccbab06e --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Services/NotificationCleanupService.cs @@ -0,0 +1,42 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using MiniSpace.Services.Notifications.Core.Repositories; +using MiniSpace.Services.Notifications.Infrastructure.Mongo.Documents; +using MiniSpace.Services.Notifications.Infrastructure.Mongo.Repositories; +using MongoDB.Driver; + +public class NotificationCleanupService : BackgroundService +{ + private readonly ILogger _logger; + private readonly IExtendedStudentNotificationsRepository _notificationsRepository; + + public NotificationCleanupService(ILogger logger, IExtendedStudentNotificationsRepository notificationsRepository) + { + _logger = logger; + _notificationsRepository = notificationsRepository; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + _logger.LogInformation("Notification Cleanup Service is running."); + await CleanupOldNotifications(); + _logger.LogInformation("Waiting 4 weeks before next cleanup."); + await Task.Delay(TimeSpan.FromDays(28), stoppingToken); + } + } + + private async Task CleanupOldNotifications() + { + var cutoffDate = DateTime.UtcNow.AddDays(-28); + var filter = Builders.Filter.ElemMatch(n => n.Notifications, n => n.CreatedAt < cutoffDate); + var update = Builders.Update.PullFilter(n => n.Notifications, n => n.CreatedAt < cutoffDate); + var result = await _notificationsRepository.BulkUpdateAsync(filter, update); + + _logger.LogInformation($"Removed {result.ModifiedCount} old notifications."); + } +} diff --git a/MiniSpace.Services.Organizations/.gitignore b/MiniSpace.Services.Organizations/.gitignore new file mode 100644 index 00000000..e263fe68 --- /dev/null +++ b/MiniSpace.Services.Organizations/.gitignore @@ -0,0 +1,331 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ +# **/Properties/launchSettings.json + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +logs/ \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/LICENSE b/MiniSpace.Services.Organizations/LICENSE new file mode 100644 index 00000000..b7ea7f0c --- /dev/null +++ b/MiniSpace.Services.Organizations/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 DevMentors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MiniSpace.Services.Organizations/MiniSpace.Services.Organizations.sln b/MiniSpace.Services.Organizations/MiniSpace.Services.Organizations.sln index ff08f2bb..4d3fd8b1 100644 --- a/MiniSpace.Services.Organizations/MiniSpace.Services.Organizations.sln +++ b/MiniSpace.Services.Organizations/MiniSpace.Services.Organizations.sln @@ -13,6 +13,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.Organiza EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.Organizations.Infrastructure", "src\MiniSpace.Services.Organizations.Infrastructure\MiniSpace.Services.Organizations.Infrastructure.csproj", "{3C5F46E5-9CF0-4A0A-AF56-9F1D455E7D1D}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{D697BE2B-E69A-40FF-9A78-0BB96CF4E6DA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.Organizations.Application.UnitTests", "tests\MiniSpace.Services.Organizations.Application.UnitTests\MiniSpace.Services.Organizations.Application.UnitTests.csproj", "{5CCAF099-8250-4DC6-AB75-95E35C9C91B3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.Organizations.Core.UnitTests", "tests\MiniSpace.Services.Organizations.Core.UnitTests\MiniSpace.Services.Organizations.Core.UnitTests.csproj", "{FBE0DFB5-3945-4A9E-B4A0-C3BED6C74726}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -38,11 +44,21 @@ Global {3C5F46E5-9CF0-4A0A-AF56-9F1D455E7D1D}.Debug|Any CPU.Build.0 = Debug|Any CPU {3C5F46E5-9CF0-4A0A-AF56-9F1D455E7D1D}.Release|Any CPU.ActiveCfg = Release|Any CPU {3C5F46E5-9CF0-4A0A-AF56-9F1D455E7D1D}.Release|Any CPU.Build.0 = Release|Any CPU + {5CCAF099-8250-4DC6-AB75-95E35C9C91B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5CCAF099-8250-4DC6-AB75-95E35C9C91B3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5CCAF099-8250-4DC6-AB75-95E35C9C91B3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5CCAF099-8250-4DC6-AB75-95E35C9C91B3}.Release|Any CPU.Build.0 = Release|Any CPU + {FBE0DFB5-3945-4A9E-B4A0-C3BED6C74726}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FBE0DFB5-3945-4A9E-B4A0-C3BED6C74726}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FBE0DFB5-3945-4A9E-B4A0-C3BED6C74726}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FBE0DFB5-3945-4A9E-B4A0-C3BED6C74726}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {D8843565-EA51-407D-9F3B-BC0741B104A0} = {9A2329ED-4000-46C9-B9CD-956F6DA767CB} {299EC532-840F-4ED9-A98F-CDC7C4DD315E} = {9A2329ED-4000-46C9-B9CD-956F6DA767CB} {550E45D1-9BE7-4AE1-BA82-DDF57F436BEB} = {9A2329ED-4000-46C9-B9CD-956F6DA767CB} {3C5F46E5-9CF0-4A0A-AF56-9F1D455E7D1D} = {9A2329ED-4000-46C9-B9CD-956F6DA767CB} + {5CCAF099-8250-4DC6-AB75-95E35C9C91B3} = {D697BE2B-E69A-40FF-9A78-0BB96CF4E6DA} + {FBE0DFB5-3945-4A9E-B4A0-C3BED6C74726} = {D697BE2B-E69A-40FF-9A78-0BB96CF4E6DA} EndGlobalSection EndGlobal diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Api/Program.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Api/Program.cs index a2a623dd..df7e9993 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Api/Program.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Api/Program.cs @@ -37,8 +37,12 @@ public static async Task Main(string[] args) .Get>("organizations/organizer/{organizerId}") .Get>("organizations/root") .Get>("organizations/{organizationId}/children") - .Post("organizations", + .Get>("organizations/{organizationId}/children/all") + .Post("organizations", afterDispatch: (cmd, ctx) => ctx.Response.Created($"organizations/root")) + .Post("organizations/{organizationId}/children", + afterDispatch: (cmd, ctx) => ctx.Response.Created($"organizations/{cmd.OrganizationId}")) + .Delete("organizations/{organizationId}") .Post("organizations/{organizationId}/organizer") .Delete("organizations/{organizationId}/organizer/{organizerId}") )) diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/AddOrganizerToOrganization.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/AddOrganizerToOrganization.cs index d50e7826..47077c04 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/AddOrganizerToOrganization.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/AddOrganizerToOrganization.cs @@ -4,11 +4,13 @@ namespace MiniSpace.Services.Organizations.Application.Commands { public class AddOrganizerToOrganization: ICommand { + public Guid RootOrganizationId { get; set; } public Guid OrganizationId { get; set; } public Guid OrganizerId { get; set; } - public AddOrganizerToOrganization(Guid organizationId, Guid organizerId) + public AddOrganizerToOrganization(Guid rootOrganizationId, Guid organizationId, Guid organizerId) { + RootOrganizationId = rootOrganizationId; OrganizationId = organizationId; OrganizerId = organizerId; } diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/AddOrganization.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/CreateOrganization.cs similarity index 65% rename from MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/AddOrganization.cs rename to MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/CreateOrganization.cs index dca309f0..083448c5 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/AddOrganization.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/CreateOrganization.cs @@ -2,16 +2,18 @@ namespace MiniSpace.Services.Organizations.Application.Commands { - public class AddOrganization: ICommand + public class CreateOrganization: ICommand { public Guid OrganizationId { get; } public string Name { get; } + public Guid RootId { get; } public Guid ParentId { get; } - public AddOrganization(Guid organizationId, string name, Guid parentId) + public CreateOrganization(Guid organizationId, string name, Guid rootId, Guid parentId) { OrganizationId = organizationId == Guid.Empty ? Guid.NewGuid() : organizationId; Name = name; + RootId = rootId; ParentId = parentId; } } diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/CreateRootOrganization.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/CreateRootOrganization.cs new file mode 100644 index 00000000..a8e63077 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/CreateRootOrganization.cs @@ -0,0 +1,16 @@ +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Organizations.Application.Commands +{ + public class CreateRootOrganization: ICommand + { + public Guid OrganizationId { get; } + public string Name { get; } + + public CreateRootOrganization(Guid organizationId, string name) + { + OrganizationId = organizationId == Guid.Empty ? Guid.NewGuid() : organizationId; + Name = name; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/DeleteOrganization.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/DeleteOrganization.cs new file mode 100644 index 00000000..d97bc948 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/DeleteOrganization.cs @@ -0,0 +1,16 @@ +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Organizations.Application.Commands +{ + public class DeleteOrganization: ICommand + { + public Guid OrganizationId { get; set; } + public Guid RootId { get; } + + public DeleteOrganization(Guid organizationId, Guid rootId) + { + OrganizationId = organizationId; + RootId = rootId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/AddOrganizerToOrganizationHandler.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/AddOrganizerToOrganizationHandler.cs index d38e6b2a..d5e3e59c 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/AddOrganizerToOrganizationHandler.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/AddOrganizerToOrganizationHandler.cs @@ -31,8 +31,14 @@ public async Task HandleAsync(AddOrganizerToOrganization command, CancellationTo throw new Exceptions.UnauthorizedAccessException("admin"); } - var organization = await _organizationRepository.GetAsync(command.OrganizationId); - if (organization is null) + var root = await _organizationRepository.GetAsync(command.RootOrganizationId); + if (root is null) + { + throw new RootOrganizationNotFoundException(command.RootOrganizationId); + } + + var organization = root.GetSubOrganization(command.OrganizationId); + if (organization == null) { throw new OrganizationNotFoundException(command.OrganizationId); } @@ -44,7 +50,7 @@ public async Task HandleAsync(AddOrganizerToOrganization command, CancellationTo } organization.AddOrganizer(command.OrganizerId); - await _organizationRepository.UpdateAsync(organization); + await _organizationRepository.UpdateAsync(root); await _messageBroker.PublishAsync(new OrganizerAddedToOrganization(organization.Id, organizer.Id)); } } diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/CreateOrganizationHandler.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/CreateOrganizationHandler.cs new file mode 100644 index 00000000..056f13b6 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/CreateOrganizationHandler.cs @@ -0,0 +1,55 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Organizations.Application.Events; +using MiniSpace.Services.Organizations.Application.Exceptions; +using MiniSpace.Services.Organizations.Application.Services; +using MiniSpace.Services.Organizations.Core.Entities; +using MiniSpace.Services.Organizations.Core.Repositories; + +namespace MiniSpace.Services.Organizations.Application.Commands.Handlers +{ + public class CreateOrganizationHandler : ICommandHandler + { + private readonly IOrganizationRepository _organizationRepository; + private readonly IAppContext _appContext; + private readonly IMessageBroker _messageBroker; + + public CreateOrganizationHandler(IOrganizationRepository organizationRepository, IAppContext appContext, + IMessageBroker messageBroker) + { + _organizationRepository = organizationRepository; + _appContext = appContext; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(CreateOrganization command, CancellationToken cancellationToken) + { + var identity = _appContext.Identity; + if(identity.IsAuthenticated && !identity.IsAdmin) + { + throw new Exceptions.UnauthorizedAccessException("admin"); + } + + var root = await _organizationRepository.GetAsync(command.RootId); + if(root is null) + { + throw new RootOrganizationNotFoundException(command.RootId); + } + + var parent = root.GetSubOrganization(command.ParentId); + if(parent is null) + { + throw new ParentOrganizationNotFoundException(command.ParentId); + } + + if (string.IsNullOrWhiteSpace(command.Name)) + { + throw new InvalidOrganizationNameException(command.Name); + } + + var organization = new Organization(command.OrganizationId, command.Name); + parent.AddSubOrganization(organization); + await _organizationRepository.UpdateAsync(root); + await _messageBroker.PublishAsync(new OrganizationCreated(organization.Id, organization.Name, parent.Id)); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/AddOrganizationHandler.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/CreateRootOrganizationHandler.cs similarity index 52% rename from MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/AddOrganizationHandler.cs rename to MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/CreateRootOrganizationHandler.cs index ede5a3d9..1d21c515 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/AddOrganizationHandler.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/CreateRootOrganizationHandler.cs @@ -1,41 +1,42 @@ using Convey.CQRS.Commands; +using MiniSpace.Services.Organizations.Application.Events; using MiniSpace.Services.Organizations.Application.Exceptions; +using MiniSpace.Services.Organizations.Application.Services; using MiniSpace.Services.Organizations.Core.Entities; using MiniSpace.Services.Organizations.Core.Repositories; namespace MiniSpace.Services.Organizations.Application.Commands.Handlers { - public class AddOrganizationHandler : ICommandHandler + public class CreateRootOrganizationHandler : ICommandHandler { private readonly IOrganizationRepository _organizationRepository; private readonly IAppContext _appContext; + private readonly IMessageBroker _messageBroker; - public AddOrganizationHandler(IOrganizationRepository organizationRepository, IAppContext appContext) + public CreateRootOrganizationHandler(IOrganizationRepository organizationRepository, IAppContext appContext, + IMessageBroker messageBroker) { _organizationRepository = organizationRepository; _appContext = appContext; + _messageBroker = messageBroker; } - public async Task HandleAsync(AddOrganization command, CancellationToken cancellationToken) + public async Task HandleAsync(CreateRootOrganization command, CancellationToken cancellationToken) { var identity = _appContext.Identity; if(identity.IsAuthenticated && !identity.IsAdmin) { throw new Exceptions.UnauthorizedAccessException("admin"); } - - var organization = new Organization(command.OrganizationId, command.Name, command.ParentId); - if(command.ParentId != Guid.Empty) + + if (string.IsNullOrWhiteSpace(command.Name)) { - var parent = await _organizationRepository.GetAsync(command.ParentId); - if(parent is null) - { - throw new ParentOrganizationNotFoundException(command.ParentId); - } - parent.MakeParent(); - await _organizationRepository.UpdateAsync(parent); + throw new InvalidOrganizationNameException(command.Name); } + + var organization = new Organization(command.OrganizationId, command.Name); await _organizationRepository.AddAsync(organization); + await _messageBroker.PublishAsync(new RootOrganizationCreated(organization.Id, organization.Name)); } } } \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/DeleteOrganizationHandler.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/DeleteOrganizationHandler.cs new file mode 100644 index 00000000..bc9530d0 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/DeleteOrganizationHandler.cs @@ -0,0 +1,55 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Organizations.Application.Events; +using MiniSpace.Services.Organizations.Application.Exceptions; +using MiniSpace.Services.Organizations.Application.Services; +using MiniSpace.Services.Organizations.Core.Repositories; + +namespace MiniSpace.Services.Organizations.Application.Commands.Handlers +{ + public class DeleteOrganizationHandler:ICommandHandler + { + private readonly IOrganizationRepository _organizationRepository; + private readonly IAppContext _appContext; + private readonly IMessageBroker _messageBroker; + + public DeleteOrganizationHandler(IOrganizationRepository organizationRepository, IAppContext appContext, IMessageBroker messageBroker) + { + _organizationRepository = organizationRepository; + _appContext = appContext; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(DeleteOrganization command, CancellationToken cancellationToken) + { + var identity = _appContext.Identity; + if(identity.IsAuthenticated && !identity.IsAdmin) + { + throw new Exceptions.UnauthorizedAccessException("admin"); + } + + var root = await _organizationRepository.GetAsync(command.RootId); + if(root is null) + { + throw new RootOrganizationNotFoundException(command.RootId); + } + + var organization = root.GetSubOrganization(command.OrganizationId); + if(organization is null) + { + throw new OrganizationNotFoundException(command.OrganizationId); + } + + if (root.Id.Equals(organization.Id)) + { + await _organizationRepository.DeleteAsync(root.Id); + } + else + { + root.RemoveChildOrganization(organization); + await _organizationRepository.UpdateAsync(root); + } + + await _messageBroker.PublishAsync(new OrganizationDeleted(organization.Id)); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/RemoveOrganizerFromOrganizationHandler.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/RemoveOrganizerFromOrganizationHandler.cs index 341fca7c..d2014590 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/RemoveOrganizerFromOrganizationHandler.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/RemoveOrganizerFromOrganizationHandler.cs @@ -29,8 +29,14 @@ public async Task HandleAsync(RemoveOrganizerFromOrganization command, Cancellat throw new Exceptions.UnauthorizedAccessException("admin"); } - var organization = await _organizationRepository.GetAsync(command.OrganizationId); - if(organization is null) + var root = await _organizationRepository.GetAsync(command.RootOrganizationId); + if (root is null) + { + throw new RootOrganizationNotFoundException(command.RootOrganizationId); + } + + var organization = root.GetSubOrganization(command.OrganizationId); + if (organization == null) { throw new OrganizationNotFoundException(command.OrganizationId); } @@ -42,7 +48,7 @@ public async Task HandleAsync(RemoveOrganizerFromOrganization command, Cancellat } organization.RemoveOrganizer(organizer.Id); - await _organizationRepository.UpdateAsync(organization); + await _organizationRepository.UpdateAsync(root); } } } \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/RemoveOrganizerFromOrganization.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/RemoveOrganizerFromOrganization.cs index 8f553940..100ada5d 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/RemoveOrganizerFromOrganization.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/RemoveOrganizerFromOrganization.cs @@ -4,11 +4,13 @@ namespace MiniSpace.Services.Organizations.Application.Commands { public class RemoveOrganizerFromOrganization : ICommand { + public Guid RootOrganizationId { get; set; } public Guid OrganizationId { get; set; } public Guid OrganizerId { get; set; } - public RemoveOrganizerFromOrganization(Guid organizationId, Guid organizerId) + public RemoveOrganizerFromOrganization(Guid rootOrganizationId, Guid organizationId, Guid organizerId) { + RootOrganizationId = rootOrganizationId; OrganizationId = organizationId; OrganizerId = organizerId; } diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationDetailsDto.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationDetailsDto.cs index a25f6c34..9c6d8904 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationDetailsDto.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationDetailsDto.cs @@ -1,13 +1,28 @@ using System.Collections; +using MiniSpace.Services.Organizations.Core.Entities; +using System.Diagnostics.CodeAnalysis; namespace MiniSpace.Services.Organizations.Application.DTO { + [ExcludeFromCodeCoverage] public class OrganizationDetailsDto { public Guid Id { get; set; } public string Name { get; set; } - public Guid ParentId { get; set; } - public bool IsLeaf { get; set; } + public Guid RootId { get; set; } public IEnumerable Organizers { get; set; } + + public OrganizationDetailsDto() + { + + } + + public OrganizationDetailsDto(Organization organization, Guid rootId) + { + Id = organization.Id; + Name = organization.Name; + RootId = rootId; + Organizers = organization.Organizers.Select(o => o.Id); + } } } \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationDto.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationDto.cs index 50ee020c..0aea03d2 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationDto.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationDto.cs @@ -1,11 +1,26 @@ +using MiniSpace.Services.Organizations.Core.Entities; +using System.Diagnostics.CodeAnalysis; + namespace MiniSpace.Services.Organizations.Application.DTO { + [ExcludeFromCodeCoverage] public class OrganizationDto { public Guid Id { get; set; } public string Name { get; set; } - public Guid ParentId { get; set; } - public bool IsLeaf { get; set; } + public Guid RootId { get; set; } + + public OrganizationDto() + { + + } + + public OrganizationDto (Organization organization, Guid rootId) + { + Id = organization.Id; + Name = organization.Name; + RootId = rootId; + } } } diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/OrganizationCreated.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/OrganizationCreated.cs new file mode 100644 index 00000000..7eebf8d4 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/OrganizationCreated.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Organizations.Application.Events +{ + public class OrganizationCreated: IEvent + { + public Guid OrganizationId { get; } + public string Name { get; } + public Guid ParentId { get; } + + public OrganizationCreated(Guid organizationId, string name, Guid parentId) + { + OrganizationId = organizationId; + Name = name; + ParentId = parentId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/OrganizationDeleted.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/OrganizationDeleted.cs new file mode 100644 index 00000000..7ec21e87 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/OrganizationDeleted.cs @@ -0,0 +1,14 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Organizations.Application.Events +{ + public class OrganizationDeleted:IEvent + { + public Guid OrganizationId { get; } + + public OrganizationDeleted(Guid organizationId) + { + OrganizationId = organizationId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/RootOrganizationCreated.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/RootOrganizationCreated.cs new file mode 100644 index 00000000..c22ace7b --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/RootOrganizationCreated.cs @@ -0,0 +1,16 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Organizations.Application.Events +{ + public class RootOrganizationCreated: IEvent + { + public Guid OrganizationId { get; } + public string Name { get; } + + public RootOrganizationCreated(Guid organizationId, string name) + { + OrganizationId = organizationId; + Name = name; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Exceptions/InvalidOrganizationNameException.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Exceptions/InvalidOrganizationNameException.cs new file mode 100644 index 00000000..14f29be8 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Exceptions/InvalidOrganizationNameException.cs @@ -0,0 +1,13 @@ +namespace MiniSpace.Services.Organizations.Application.Exceptions +{ + public class InvalidOrganizationNameException : AppException + { + public override string Code { get; } = "invalid_organization_name"; + public string Name { get; } + + public InvalidOrganizationNameException(string name) : base($"Invalid organization name: {name}.") + { + Name = name; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Exceptions/RootOrganizationNotFoundException.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Exceptions/RootOrganizationNotFoundException.cs new file mode 100644 index 00000000..afe37377 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Exceptions/RootOrganizationNotFoundException.cs @@ -0,0 +1,13 @@ +namespace MiniSpace.Services.Organizations.Application.Exceptions +{ + public class RootOrganizationNotFoundException : AppException + { + public override string Code { get; } = "root_organization_not_found"; + public Guid OrganizationId { get; } + + public RootOrganizationNotFoundException(Guid organizationId) : base($"Root organization with ID: '{organizationId}' was not found.") + { + OrganizationId = organizationId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetAllChildrenOrganizations.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetAllChildrenOrganizations.cs new file mode 100644 index 00000000..dc365f69 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetAllChildrenOrganizations.cs @@ -0,0 +1,13 @@ +using Convey.CQRS.Queries; +using MiniSpace.Services.Organizations.Application.DTO; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Organizations.Application.Queries +{ + [ExcludeFromCodeCoverage] + public class GetAllChildrenOrganizations: IQuery> + { + public Guid OrganizationId { get; set; } + public Guid RootId { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetChildrenOrganizations.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetChildrenOrganizations.cs index a71d10e0..adaf8bf0 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetChildrenOrganizations.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetChildrenOrganizations.cs @@ -1,10 +1,13 @@ using Convey.CQRS.Queries; using MiniSpace.Services.Organizations.Application.DTO; +using System.Diagnostics.CodeAnalysis; namespace MiniSpace.Services.Organizations.Application.Queries { + [ExcludeFromCodeCoverage] public class GetChildrenOrganizations: IQuery> { - public Guid ParentId { get; set; } + public Guid OrganizationId { get; set; } + public Guid RootId { get; set; } } } \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetOrganization.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetOrganization.cs index cbcb8848..d39c8f60 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetOrganization.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetOrganization.cs @@ -1,10 +1,13 @@ using Convey.CQRS.Queries; using MiniSpace.Services.Organizations.Application.DTO; +using System.Diagnostics.CodeAnalysis; namespace MiniSpace.Services.Organizations.Application.Queries { + [ExcludeFromCodeCoverage] public class GetOrganization : IQuery { public Guid OrganizationId { get; set; } + public Guid RootId { get; set; } } } \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetOrganizationDetails.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetOrganizationDetails.cs index 46b266a0..949bf2da 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetOrganizationDetails.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetOrganizationDetails.cs @@ -1,10 +1,13 @@ using Convey.CQRS.Queries; using MiniSpace.Services.Organizations.Application.DTO; +using System.Diagnostics.CodeAnalysis; namespace MiniSpace.Services.Organizations.Application.Queries { + [ExcludeFromCodeCoverage] public class GetOrganizationDetails : IQuery { public Guid OrganizationId { get; set; } + public Guid RootId { get; set; } } } \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetOrganizerOrganizations.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetOrganizerOrganizations.cs index 327e8da5..2aac41aa 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetOrganizerOrganizations.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetOrganizerOrganizations.cs @@ -1,11 +1,17 @@ using Convey.CQRS.Queries; using MiniSpace.Services.Organizations.Application.DTO; +using System.Diagnostics.CodeAnalysis; + namespace MiniSpace.Services.Organizations.Application.Queries { + [ExcludeFromCodeCoverage] + public class GetOrganizerOrganizations: IQuery> { + public Guid OrganizerId { get; set; } + public Guid RootId { get; set; } } } diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetRootOrganizations.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetRootOrganizations.cs index ca798f96..9b49d083 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetRootOrganizations.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetRootOrganizations.cs @@ -1,8 +1,11 @@ using Convey.CQRS.Queries; using MiniSpace.Services.Organizations.Application.DTO; +using System.Diagnostics.CodeAnalysis; + namespace MiniSpace.Services.Organizations.Application.Queries { + [ExcludeFromCodeCoverage] public class GetRootOrganizations: IQuery> { } diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/Organization.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/Organization.cs index ff915096..ee3282b0 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/Organization.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/Organization.cs @@ -5,9 +5,8 @@ namespace MiniSpace.Services.Organizations.Core.Entities public class Organization : AggregateRoot { private ISet _organizers = new HashSet(); + private ISet _subOrganizations = new HashSet(); public string Name { get; private set; } - public Guid ParentId { get; private set; } - public bool IsLeaf { get; private set; } public IEnumerable Organizers { @@ -15,13 +14,28 @@ public IEnumerable Organizers private set => _organizers = new HashSet(value); } - public Organization(Guid id, string name, Guid parentId, bool isLeaf = true, IEnumerable organizers = null) + public IEnumerable SubOrganizations + { + get => _subOrganizations; + private set => _subOrganizations = new HashSet(value); + } + + public Organization(Guid id, string name, IEnumerable organizationOrganizers = null, + IEnumerable organizations = null) { Id = id; Name = name; - ParentId = parentId; - IsLeaf = isLeaf; - Organizers = organizers ?? Enumerable.Empty(); + Organizers = organizationOrganizers ?? Enumerable.Empty(); + SubOrganizations = organizations ?? Enumerable.Empty(); + } + + public void AddOrganizer(Guid organizerId) + { + if(Organizers.Any(x => x.Id == organizerId)) + { + throw new OrganizerAlreadyAddedToOrganizationException(organizerId, Id); + } + _organizers.Add(new Organizer(organizerId)); } public void RemoveOrganizer(Guid organizerId) @@ -34,18 +48,94 @@ public void RemoveOrganizer(Guid organizerId) _organizers.Remove(organizer); } - public void AddOrganizer(Guid organizerId) + public Organization GetSubOrganization(Guid id) { - if(Organizers.Any(x => x.Id == organizerId)) + if (Id == id) { - throw new OrganizerAlreadyAddedToOrganizationException(organizerId, Id); + return this; + } + + foreach (var subOrg in SubOrganizations) + { + var result = subOrg.GetSubOrganization(id); + if (result != null) + { + return result; + } + } + + return null; + } + + public void AddSubOrganization(Organization organization) + => _subOrganizations.Add(organization); + + public static List FindOrganizations(Guid targetOrganizerId, Organization rootOrganization) + { + var organizations = new List(); + FindOrganizationsRecursive(targetOrganizerId, rootOrganization, organizations); + return organizations; + } + + private static void FindOrganizationsRecursive(Guid targetOrganizerId, Organization currentOrganization, + ICollection organizations) + { + if (currentOrganization.Organizers.Any(x => x.Id == targetOrganizerId)) + { + organizations.Add(currentOrganization); + } + + foreach (var subOrg in currentOrganization.SubOrganizations) + { + FindOrganizationsRecursive(targetOrganizerId, subOrg, organizations); } - _organizers.Add(new Organizer(organizerId)); } + public static List FindAllChildrenOrganizations(Organization rootOrganization) + { + var organizations = new List(); + FindAllChildrenOrganizationsRecursive(rootOrganization, organizations); + return organizations; + } + private static void FindAllChildrenOrganizationsRecursive(Organization currentOrganization, + ICollection organizations) + { + organizations.Add(currentOrganization.Id); + + foreach (var subOrg in currentOrganization.SubOrganizations) + { + FindAllChildrenOrganizationsRecursive(subOrg, organizations); + } + } - public void MakeParent() - => IsLeaf = false; + private Organization GetParentOrganization(Guid id) + { + foreach (var subOrg in SubOrganizations) + { + if (subOrg.Id == id) + { + return this; + } + + var result = subOrg.GetParentOrganization(id); + if (result != null) + { + return result; + } + } + + return null; + } + + public void RemoveChildOrganization(Organization organization) + { + var parent = GetParentOrganization(organization.Id); + if(parent is null) + { + throw new ParentOfOrganizationNotFoundException(organization.Id); + } + parent._subOrganizations.Remove(organization); + } } } \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Exceptions/ParentOfOrganizationNotFoundException.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Exceptions/ParentOfOrganizationNotFoundException.cs new file mode 100644 index 00000000..92f683a0 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Exceptions/ParentOfOrganizationNotFoundException.cs @@ -0,0 +1,14 @@ +namespace MiniSpace.Services.Organizations.Core.Exceptions +{ + public class ParentOfOrganizationNotFoundException : DomainException + { + public override string Code { get; } = "parent_of_organization_not_found"; + public Guid ChildId { get; } + + public ParentOfOrganizationNotFoundException(Guid childId) : base( + $"Parent organization was not found for child organization with ID: '{childId}'.") + { + ChildId = childId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Contexts/AppContext.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Contexts/AppContext.cs index ccb1647b..6843cf90 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Contexts/AppContext.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Contexts/AppContext.cs @@ -1,7 +1,9 @@ using MiniSpace.Services.Organizations.Application; +using System.Diagnostics.CodeAnalysis; namespace MiniSpace.Services.Organizations.Infrastructure.Contexts { + [ExcludeFromCodeCoverage] internal class AppContext : IAppContext { public string RequestId { get; } diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Contexts/AppContextFactory.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Contexts/AppContextFactory.cs index ac2758b9..4d96dc46 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Contexts/AppContextFactory.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Contexts/AppContextFactory.cs @@ -2,9 +2,11 @@ using Microsoft.AspNetCore.Http; using Newtonsoft.Json; using MiniSpace.Services.Organizations.Application; +using System.Diagnostics.CodeAnalysis; namespace MiniSpace.Services.Organizations.Infrastructure.Contexts { + [ExcludeFromCodeCoverage] internal sealed class AppContextFactory : IAppContextFactory { private readonly ICorrelationContextAccessor _contextAccessor; diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Contexts/CorrelationContext.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Contexts/CorrelationContext.cs index cc5f8b30..aa0ebbc5 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Contexts/CorrelationContext.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Contexts/CorrelationContext.cs @@ -1,5 +1,8 @@ +using System.Diagnostics.CodeAnalysis; + namespace MiniSpace.Services.Organizations.Infrastructure.Contexts { + [ExcludeFromCodeCoverage] internal class CorrelationContext { public string CorrelationId { get; set; } diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Contexts/IdentityContext.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Contexts/IdentityContext.cs index cc69bd58..18e9fddf 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Contexts/IdentityContext.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Contexts/IdentityContext.cs @@ -1,7 +1,12 @@ using MiniSpace.Services.Organizations.Application; +using System.Runtime.CompilerServices; +using System.Diagnostics.CodeAnalysis; + +[assembly: InternalsVisibleTo("MiniSpace.Services.Organizations.Application.UnitTests")] namespace MiniSpace.Services.Organizations.Infrastructure.Contexts { + [ExcludeFromCodeCoverage] internal class IdentityContext : IIdentityContext { public Guid Id { get; } diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Decorators/OutboxCommandHandlerDecorator.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Decorators/OutboxCommandHandlerDecorator.cs index 4fa3dff8..247c6452 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Decorators/OutboxCommandHandlerDecorator.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Decorators/OutboxCommandHandlerDecorator.cs @@ -2,10 +2,12 @@ using Convey.MessageBrokers; using Convey.MessageBrokers.Outbox; using Convey.Types; +using System.Diagnostics.CodeAnalysis; namespace MiniSpace.Services.Organizations.Infrastructure.Decorators { [Decorator] + [ExcludeFromCodeCoverage] internal sealed class OutboxCommandHandlerDecorator : ICommandHandler where TCommand : class, ICommand { diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Decorators/OutboxEventHandlerDecorator.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Decorators/OutboxEventHandlerDecorator.cs index 478dcf8e..cbe3777d 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Decorators/OutboxEventHandlerDecorator.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Decorators/OutboxEventHandlerDecorator.cs @@ -2,10 +2,12 @@ using Convey.MessageBrokers; using Convey.MessageBrokers.Outbox; using Convey.Types; +using System.Diagnostics.CodeAnalysis; namespace MiniSpace.Services.Organizations.Infrastructure.Decorators { [Decorator] + [ExcludeFromCodeCoverage] internal sealed class OutboxEventHandlerDecorator : IEventHandler where TEvent : class, IEvent { diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Exceptions/ExceptionToMessageMapper.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Exceptions/ExceptionToMessageMapper.cs index c69dff3a..62ec3951 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Exceptions/ExceptionToMessageMapper.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Exceptions/ExceptionToMessageMapper.cs @@ -4,9 +4,11 @@ // using MiniSpace.Services.Organizations.Application.Events.External; // using MiniSpace.Services.Organizations.Application.Exceptions; // using MiniSpace.Services.Organizations.Core; +using System.Diagnostics.CodeAnalysis; namespace MiniSpace.Services.Organizations.Infrastructure.Exceptions { + [ExcludeFromCodeCoverage] internal sealed class ExceptionToMessageMapper : IExceptionToMessageMapper { public object Map(Exception exception, object message) diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Exceptions/ExceptionToResponseMapper.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Exceptions/ExceptionToResponseMapper.cs index a0d24d1b..e6e53792 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Exceptions/ExceptionToResponseMapper.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Exceptions/ExceptionToResponseMapper.cs @@ -4,9 +4,11 @@ using Convey.WebApi.Exceptions; using MiniSpace.Services.Organizations.Application.Exceptions; using MiniSpace.Services.Organizations.Core.Exceptions; +using System.Diagnostics.CodeAnalysis; namespace MiniSpace.Services.Organizations.Infrastructure.Exceptions { + [ExcludeFromCodeCoverage] internal sealed class ExceptionToResponseMapper : IExceptionToResponseMapper { private static readonly ConcurrentDictionary Codes = new ConcurrentDictionary(); diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Extensions.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Extensions.cs index f020d0ce..b24efa36 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Extensions.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Extensions.cs @@ -39,9 +39,11 @@ using MiniSpace.Services.Organizations.Infrastructure.Mongo.Documents; using MiniSpace.Services.Organizations.Infrastructure.Mongo.Repositories; using MiniSpace.Services.Organizations.Infrastructure.Services; +using System.Diagnostics.CodeAnalysis; namespace MiniSpace.Services.Organizations.Infrastructure { + [ExcludeFromCodeCoverage] public static class Extensions { public static IConveyBuilder AddInfrastructure(this IConveyBuilder builder) @@ -88,7 +90,8 @@ public static IApplicationBuilder UseInfrastructure(this IApplicationBuilder app .UseMetrics() .UseCertificateAuthentication() .UseRabbitMq() - .SubscribeCommand() + .SubscribeCommand() + .SubscribeCommand() .SubscribeCommand() .SubscribeCommand() .SubscribeEvent() diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Logging/Extensions.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Logging/Extensions.cs index 82729734..5feb4a9a 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Logging/Extensions.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Logging/Extensions.cs @@ -2,14 +2,16 @@ using Convey.Logging.CQRS; using Microsoft.Extensions.DependencyInjection; using MiniSpace.Services.Organizations.Application.Commands; +using System.Diagnostics.CodeAnalysis; namespace MiniSpace.Services.Organizations.Infrastructure.Logging { + [ExcludeFromCodeCoverage] internal static class Extensions { public static IConveyBuilder AddHandlersLogging(this IConveyBuilder builder) { - var assembly = typeof(AddOrganization).Assembly; + var assembly = typeof(CreateOrganization).Assembly; builder.Services.AddSingleton(new MessageToLogTemplateMapper()); diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Logging/MessageToLogTemplateMapper.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Logging/MessageToLogTemplateMapper.cs index 598978f1..82111a8f 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Logging/MessageToLogTemplateMapper.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Logging/MessageToLogTemplateMapper.cs @@ -1,18 +1,32 @@ using Convey.Logging.CQRS; using MiniSpace.Services.Organizations.Application.Commands; using MiniSpace.Services.Organizations.Application.Events.External; +using System.Diagnostics.CodeAnalysis; namespace MiniSpace.Services.Organizations.Infrastructure.Logging { + [ExcludeFromCodeCoverage] internal sealed class MessageToLogTemplateMapper : IMessageToLogTemplateMapper { private static IReadOnlyDictionary MessageTemplates => new Dictionary { { - typeof(AddOrganization), new HandlerLogTemplate + typeof(CreateRootOrganization), new HandlerLogTemplate { - After = "Added a new organization with id: {OrganizationId}." + After = "Created a new root organization with id: {OrganizationId}." + } + }, + { + typeof(CreateOrganization), new HandlerLogTemplate + { + After = "Added a new child organization with id: {OrganizationId} for parent with id: {ParentId}." + } + }, + { + typeof(DeleteOrganization), new HandlerLogTemplate + { + After = "Deleted an organization with id: {OrganizationId} and its children." } }, { diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Documents/Extensions.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Documents/Extensions.cs index 6322ec5c..11e9f07c 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Documents/Extensions.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Documents/Extensions.cs @@ -1,39 +1,39 @@ using MiniSpace.Services.Organizations.Application.DTO; using MiniSpace.Services.Organizations.Core.Entities; +using System.Diagnostics.CodeAnalysis; namespace MiniSpace.Services.Organizations.Infrastructure.Mongo.Documents { + [ExcludeFromCodeCoverage] + public static class Extensions { public static Organization AsEntity(this OrganizationDocument document) - => new Organization(document.Id, document.Name, document.ParentId, document.IsLeaf, document.Organizers); + => new Organization(document.Id, document.Name, document.Organizers, document.SubOrganizations.Select(o => o.AsEntity())); public static OrganizationDocument AsDocument(this Organization entity) => new OrganizationDocument() { Id = entity.Id, Name = entity.Name, - ParentId = entity.ParentId, - IsLeaf = entity.IsLeaf, - Organizers = entity.Organizers + Organizers = entity.Organizers, + SubOrganizations = entity.SubOrganizations.Select(o => o.AsDocument()) }; - public static OrganizationDto AsDto(this OrganizationDocument document) + public static OrganizationDto AsDto(this OrganizationDocument document, Guid rootId) => new OrganizationDto() { Id = document.Id, Name = document.Name, - ParentId = document.ParentId, - IsLeaf = document.IsLeaf + RootId = rootId }; - public static OrganizationDetailsDto AsDetailsDto(this OrganizationDocument document) + public static OrganizationDetailsDto AsDetailsDto(this OrganizationDocument document, Guid rootId) => new OrganizationDetailsDto() { Id = document.Id, Name = document.Name, - ParentId = document.ParentId, - IsLeaf = document.IsLeaf, + RootId = rootId, Organizers = document.Organizers.Select(x => x.Id) }; diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Documents/OrganizationDocument.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Documents/OrganizationDocument.cs index 24946e59..375f3a0f 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Documents/OrganizationDocument.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Documents/OrganizationDocument.cs @@ -1,14 +1,16 @@ using Convey.Types; using MiniSpace.Services.Organizations.Core.Entities; +using System.Diagnostics.CodeAnalysis; namespace MiniSpace.Services.Organizations.Infrastructure.Mongo.Documents { + [ExcludeFromCodeCoverage] + public class OrganizationDocument: IIdentifiable { public Guid Id { get; set; } public string Name { get; set; } - public Guid ParentId { get; set; } - public bool IsLeaf { get; set; } public IEnumerable Organizers { get; set; } + public IEnumerable SubOrganizations { get; set; } } } \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Documents/OrganizerDocument.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Documents/OrganizerDocument.cs index 8e6411ce..0345c012 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Documents/OrganizerDocument.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Documents/OrganizerDocument.cs @@ -1,7 +1,9 @@ using Convey.Types; +using System.Diagnostics.CodeAnalysis; namespace MiniSpace.Services.Organizations.Infrastructure.Mongo.Documents { + [ExcludeFromCodeCoverage] public class OrganizerDocument : IIdentifiable { public Guid Id { get; set; } diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetAllChildrenOrganizationsHandler.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetAllChildrenOrganizationsHandler.cs new file mode 100644 index 00000000..67babbb4 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetAllChildrenOrganizationsHandler.cs @@ -0,0 +1,31 @@ +using Convey.CQRS.Queries; +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Organizations.Application.Queries; +using MiniSpace.Services.Organizations.Core.Entities; +using MiniSpace.Services.Organizations.Infrastructure.Mongo.Documents; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Organizations.Infrastructure.Mongo.Queries.Handlers +{ + [ExcludeFromCodeCoverage] + public class GetAllChildrenOrganizationsHandler : IQueryHandler> + { + private readonly IMongoRepository _repository; + + public GetAllChildrenOrganizationsHandler(IMongoRepository repository) + => _repository = repository; + + public async Task> HandleAsync(GetAllChildrenOrganizations query, CancellationToken cancellationToken) + { + var root = await _repository.GetAsync(o => o.Id == query.RootId); + var organization = root?.AsEntity().GetSubOrganization(query.OrganizationId); + var result = new List(); + if (organization != null) + { + result.AddRange(Organization.FindAllChildrenOrganizations(organization)); + } + + return result; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetChildrenOrganizationsHandler.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetChildrenOrganizationsHandler.cs index ab868a7d..28401dcd 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetChildrenOrganizationsHandler.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetChildrenOrganizationsHandler.cs @@ -4,9 +4,11 @@ using MiniSpace.Services.Organizations.Application.Queries; using MiniSpace.Services.Organizations.Core.Entities; using MiniSpace.Services.Organizations.Infrastructure.Mongo.Documents; +using System.Diagnostics.CodeAnalysis; namespace MiniSpace.Services.Organizations.Infrastructure.Mongo.Queries.Handlers { + [ExcludeFromCodeCoverage] public class GetChildrenOrganizationsHandler : IQueryHandler> { private readonly IMongoRepository _repository; @@ -16,9 +18,15 @@ public GetChildrenOrganizationsHandler(IMongoRepository> HandleAsync(GetChildrenOrganizations query, CancellationToken cancellationToken) { - var organizations = await _repository.FindAsync(o => o.ParentId == query.ParentId); + var root = await _repository.GetAsync(o => o.Id == query.RootId); + if (root == null) + { + return Enumerable.Empty(); + } - return organizations.Select(o => o.AsDto()); + var parent = root.AsEntity().GetSubOrganization(query.OrganizationId); + return parent == null ? Enumerable.Empty() + : parent.SubOrganizations.Select(o => new OrganizationDto(o, root.Id)); } } } \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetOrganizationDetailsHandler.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetOrganizationDetailsHandler.cs index dd249add..d78c2a00 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetOrganizationDetailsHandler.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetOrganizationDetailsHandler.cs @@ -4,9 +4,12 @@ using MiniSpace.Services.Organizations.Application.Queries; using MiniSpace.Services.Organizations.Infrastructure.Mongo.Documents; using MongoDB.Driver; +using System.Diagnostics.CodeAnalysis; + namespace MiniSpace.Services.Organizations.Infrastructure.Mongo.Queries.Handlers { + [ExcludeFromCodeCoverage] public class GetOrganizationDetailsHandler : IQueryHandler { private readonly IMongoRepository _repository; @@ -18,9 +21,9 @@ public GetOrganizationDetailsHandler(IMongoRepository HandleAsync(GetOrganizationDetails query, CancellationToken cancellationToken) { - var organization = await _repository.GetAsync(query.OrganizationId); - - return organization?.AsDetailsDto(); + var root = await _repository.GetAsync(o => o.Id == query.RootId); + var organization = root?.AsEntity().GetSubOrganization(query.OrganizationId); + return organization == null ? null : new OrganizationDetailsDto(organization, root.Id); } } } \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetOrganizationHandler.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetOrganizationHandler.cs index d8cb184f..8978aa5c 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetOrganizationHandler.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetOrganizationHandler.cs @@ -3,9 +3,12 @@ using MiniSpace.Services.Organizations.Application.DTO; using MiniSpace.Services.Organizations.Application.Queries; using MiniSpace.Services.Organizations.Infrastructure.Mongo.Documents; +using System.Diagnostics.CodeAnalysis; + namespace MiniSpace.Services.Organizations.Infrastructure.Mongo.Queries.Handlers { + [ExcludeFromCodeCoverage] public class GetOrganizationHandler : IQueryHandler { private readonly IMongoRepository _repository; @@ -17,9 +20,9 @@ public GetOrganizationHandler(IMongoRepository repos public async Task HandleAsync(GetOrganization query, CancellationToken cancellationToken) { - var organization = await _repository.GetAsync(query.OrganizationId); - - return organization?.AsDto(); + var root = await _repository.GetAsync(o => o.Id == query.RootId); + var organization = root?.AsEntity().GetSubOrganization(query.OrganizationId); + return organization == null ? null : new OrganizationDto(organization, root.Id); } } } \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetOrganizerOrganizationsHandler.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetOrganizerOrganizationsHandler.cs index 0a587fbf..e12bede2 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetOrganizerOrganizationsHandler.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetOrganizerOrganizationsHandler.cs @@ -5,9 +5,11 @@ using MiniSpace.Services.Organizations.Application.Queries; using MiniSpace.Services.Organizations.Core.Entities; using MiniSpace.Services.Organizations.Infrastructure.Mongo.Documents; +using System.Diagnostics.CodeAnalysis; namespace MiniSpace.Services.Organizations.Infrastructure.Mongo.Queries.Handlers { + [ExcludeFromCodeCoverage] public class GetOrganizerOrganizationsHandler : IQueryHandler> { private readonly IMongoRepository _repository; @@ -27,10 +29,15 @@ public async Task> HandleAsync(GetOrganizerOrganiza return Enumerable.Empty(); } - var organizations = await _repository - .FindAsync(o => o.Organizers.Any(x => x.Id == query.OrganizerId)); + var roots = (await _repository.FindAsync(o => true)).Select(o =>o.AsEntity()); + var organizerOrganizations = new List(); + foreach (var root in roots) + { + var organizations = Organization.FindOrganizations(query.OrganizerId, root); + organizerOrganizations.AddRange(organizations.Select(o => new OrganizationDto(o, root.Id))); + } - return organizations.Select(o => o.AsDto()); + return organizerOrganizations; } } } \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetRootOrganizationsHandler.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetRootOrganizationsHandler.cs index a4ca6c3c..05f94059 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetRootOrganizationsHandler.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetRootOrganizationsHandler.cs @@ -3,9 +3,11 @@ using MiniSpace.Services.Organizations.Application.DTO; using MiniSpace.Services.Organizations.Application.Queries; using MiniSpace.Services.Organizations.Infrastructure.Mongo.Documents; +using System.Diagnostics.CodeAnalysis; namespace MiniSpace.Services.Organizations.Infrastructure.Mongo.Queries.Handlers { + [ExcludeFromCodeCoverage] public class GetRootOrganizationsHandler : IQueryHandler> { private readonly IMongoRepository _repository; @@ -16,6 +18,6 @@ public GetRootOrganizationsHandler(IMongoRepository } public async Task> HandleAsync(GetRootOrganizations query, CancellationToken cancellationToken) - => (await _repository.FindAsync(o => o.ParentId == Guid.Empty)).Select(o => o.AsDto()); + => (await _repository.FindAsync(o => true)).Select(o =>o.AsDto(o.Id)); } } \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Repositories/OrganizationMongoRepository.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Repositories/OrganizationMongoRepository.cs index 452841d0..506c1853 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Repositories/OrganizationMongoRepository.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Repositories/OrganizationMongoRepository.cs @@ -2,9 +2,11 @@ using MiniSpace.Services.Organizations.Core.Entities; using MiniSpace.Services.Organizations.Core.Repositories; using MiniSpace.Services.Organizations.Infrastructure.Mongo.Documents; +using System.Diagnostics.CodeAnalysis; namespace MiniSpace.Services.Organizations.Infrastructure.Mongo.Repositories { + [ExcludeFromCodeCoverage] public class OrganizationMongoRepository : IOrganizationRepository { private readonly IMongoRepository _repository; diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Repositories/OrganizerMongoRepository.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Repositories/OrganizerMongoRepository.cs index bc7cc797..88f14835 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Repositories/OrganizerMongoRepository.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Repositories/OrganizerMongoRepository.cs @@ -4,9 +4,11 @@ using MiniSpace.Services.Organizations.Core.Entities; using MiniSpace.Services.Organizations.Core.Repositories; using MiniSpace.Services.Organizations.Infrastructure.Mongo.Documents; +using System.Diagnostics.CodeAnalysis; namespace MiniSpace.Services.Organizations.Infrastructure.Mongo.Repositories { + [ExcludeFromCodeCoverage] public class OrganizerMongoRepository : IOrganizerRepository { private readonly IMongoRepository _repository; diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Services/DateTimeProvider.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Services/DateTimeProvider.cs index 0da02bcd..b3f41508 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Services/DateTimeProvider.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Services/DateTimeProvider.cs @@ -1,7 +1,9 @@ using MiniSpace.Services.Organizations.Application.Services; +using System.Diagnostics.CodeAnalysis; namespace MiniSpace.Services.Organizations.Infrastructure.Services { + [ExcludeFromCodeCoverage] internal sealed class DateTimeProvider : IDateTimeProvider { public DateTime Now => DateTime.UtcNow; diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Services/EventMapper.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Services/EventMapper.cs index 7b2ca717..63237369 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Services/EventMapper.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Services/EventMapper.cs @@ -2,9 +2,11 @@ using MiniSpace.Services.Organizations.Application.Services; using MiniSpace.Services.Organizations.Core; using MiniSpace.Services.Organizations.Core.Events; +using System.Diagnostics.CodeAnalysis; namespace MiniSpace.Services.Organizations.Infrastructure.Services { + [ExcludeFromCodeCoverage] public class EventMapper : IEventMapper { public IEnumerable MapAll(IEnumerable events) diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Services/MessageBroker.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Services/MessageBroker.cs index 2390268f..0601896b 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Services/MessageBroker.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Services/MessageBroker.cs @@ -6,9 +6,11 @@ using Microsoft.Extensions.Logging; using OpenTracing; using MiniSpace.Services.Organizations.Application.Services; +using System.Diagnostics.CodeAnalysis; namespace MiniSpace.Services.Organizations.Infrastructure.Services { + [ExcludeFromCodeCoverage] internal sealed class MessageBroker : IMessageBroker { private const string DefaultSpanContextHeader = "span_context"; diff --git a/MiniSpace.Services.Organizations/tests/MiniSpace.Services.Organizations.Application.UnitTests/Commands/Handlers/AddOrganizerToOrganizationHandlerTest.cs b/MiniSpace.Services.Organizations/tests/MiniSpace.Services.Organizations.Application.UnitTests/Commands/Handlers/AddOrganizerToOrganizationHandlerTest.cs new file mode 100644 index 00000000..2ece87ea --- /dev/null +++ b/MiniSpace.Services.Organizations/tests/MiniSpace.Services.Organizations.Application.UnitTests/Commands/Handlers/AddOrganizerToOrganizationHandlerTest.cs @@ -0,0 +1,177 @@ +using Xunit; +using Moq; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Threading; +using FluentAssertions; +using MiniSpace.Services.Organizations.Application.Commands.Handlers; +using MiniSpace.Services.Organizations.Core.Repositories; +using MiniSpace.Services.Organizations.Application.Services; +using MiniSpace.Services.Organizations.Core.Entities; +using MiniSpace.Services.Organizations.Infrastructure.Contexts; +using MiniSpace.Services.Organizations.Application.Commands; +using MiniSpace.Services.Organizations.Application.Exceptions; + +namespace MiniSpace.Services.Organizations.Application.UnitTests.Commands.Handlers +{ + public class AddOrganizerToOrganizationHandlerTest + { + private readonly AddOrganizerToOrganizationHandler _addOrganizerToOrganizationHandler; + private readonly Mock _organizationRepositoryMock; + private readonly Mock _organizerRepositoryMock; + private readonly Mock _appContextMock; + private readonly Mock _messageBrokerMock; + + public AddOrganizerToOrganizationHandlerTest() + { + _organizationRepositoryMock = new Mock(); + _organizerRepositoryMock = new Mock(); + _appContextMock = new Mock(); + _messageBrokerMock = new Mock(); + _addOrganizerToOrganizationHandler = new AddOrganizerToOrganizationHandler( + _organizationRepositoryMock.Object, + _organizerRepositoryMock.Object, + _appContextMock.Object, + _messageBrokerMock.Object + ); + } + + [Fact] + public async Task HandleAsync_WithValidOrganizationAndAuthorised_ShouldUpdateOrganization() + { + // Arrange + var rootOrganizationId = Guid.NewGuid(); + var organizationId = Guid.NewGuid(); + var organizerId = Guid.NewGuid(); + var comand = new AddOrganizerToOrganization(rootOrganizationId, organizationId, organizerId); + + var organization = new Organization(organizationId, "this", new List() { }, new List() { }); + var rootOrganization = new Organization(rootOrganizationId, "root", new List() { }, new List() { organization }); + var organizer = new Organizer(organizerId); + + + var identityContext = new IdentityContext(Guid.NewGuid().ToString(), "Admin", true, new Dictionary()); + + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + _organizationRepositoryMock.Setup(repo => repo.GetAsync(comand.RootOrganizationId)).ReturnsAsync(rootOrganization); + _organizerRepositoryMock.Setup(repo => repo.GetAsync(comand.OrganizerId)).ReturnsAsync(organizer); + + var cancelationToken = new CancellationToken(); + + // Act + await _addOrganizerToOrganizationHandler.HandleAsync(comand, cancelationToken); + + // Assert + _organizationRepositoryMock.Verify(repo => repo.UpdateAsync(rootOrganization), Times.Once()); + } + + [Fact] + public async Task HandleAsync_WithoutAdminRole_ShouldThrowUnauthorizedAccessException() + { + // Arrange + var rootOrganizationId = Guid.NewGuid(); + var organizationId = Guid.NewGuid(); + var organizerId = Guid.NewGuid(); + var comand = new AddOrganizerToOrganization(rootOrganizationId, organizationId, organizerId); + + var organization = new Organization(organizationId, "this", new List() { }, new List() { }); + var rootOrganization = new Organization(rootOrganizationId, "root", new List() { }, new List() { organization }); + var organizer = new Organizer(organizerId); + + + var identityContext = new IdentityContext(Guid.NewGuid().ToString(), "", true, new Dictionary()); + + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + _organizationRepositoryMock.Setup(repo => repo.GetAsync(comand.RootOrganizationId)).ReturnsAsync(rootOrganization); + _organizerRepositoryMock.Setup(repo => repo.GetAsync(comand.OrganizerId)).ReturnsAsync(organizer); + + var cancelationToken = new CancellationToken(); + + // Act & Assert + Func act = async () => await _addOrganizerToOrganizationHandler.HandleAsync(comand, cancelationToken); + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task HandleAsync_InavalidOrganizationRoot_ShouldThrowRootOrganizationNotFoundException() + { + // Arrange + var rootOrganizationId = Guid.NewGuid(); + var organizationId = Guid.NewGuid(); + var organizerId = Guid.NewGuid(); + var comand = new AddOrganizerToOrganization(rootOrganizationId, organizationId, organizerId); + + var organization = new Organization(organizationId, "this", new List() { }, new List() { }); + var rootOrganization = new Organization(rootOrganizationId, "root", new List() { }, new List() { organization }); + var organizer = new Organizer(organizerId); + + + var identityContext = new IdentityContext(Guid.NewGuid().ToString(), "Admin", true, new Dictionary()); + + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + _organizationRepositoryMock.Setup(repo => repo.GetAsync(comand.RootOrganizationId)).ReturnsAsync((Organization)null); + _organizerRepositoryMock.Setup(repo => repo.GetAsync(comand.OrganizerId)).ReturnsAsync(organizer); + + var cancelationToken = new CancellationToken(); + + // Act & Assert + Func act = async () => await _addOrganizerToOrganizationHandler.HandleAsync(comand, cancelationToken); + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task HandleAsync_WithoutOrganizationInOrganizationRoot_ShouldThrowOrganizationNotFoundException() + { + // Arrange + var rootOrganizationId = Guid.NewGuid(); + var organizationId = Guid.NewGuid(); + var organizerId = Guid.NewGuid(); + var comand = new AddOrganizerToOrganization(rootOrganizationId, organizationId, organizerId); + + var organization = new Organization(organizationId, "this", new List() { }, new List() { }); + var rootOrganization = new Organization(rootOrganizationId, "root", new List() { }, new List() { }); + var organizer = new Organizer(organizerId); + + + var identityContext = new IdentityContext(Guid.NewGuid().ToString(), "Admin", true, new Dictionary()); + + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + _organizationRepositoryMock.Setup(repo => repo.GetAsync(comand.RootOrganizationId)).ReturnsAsync(rootOrganization); + _organizerRepositoryMock.Setup(repo => repo.GetAsync(comand.OrganizerId)).ReturnsAsync(organizer); + + var cancelationToken = new CancellationToken(); + + // Act & Assert + Func act = async () => await _addOrganizerToOrganizationHandler.HandleAsync(comand, cancelationToken); + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task HandleAsync_WithInvalidOrganizer_ShouldThrowOrganizerNotFoundException() + { + // Arrange + var rootOrganizationId = Guid.NewGuid(); + var organizationId = Guid.NewGuid(); + var organizerId = Guid.NewGuid(); + var comand = new AddOrganizerToOrganization(rootOrganizationId, organizationId, organizerId); + + var organization = new Organization(organizationId, "this", new List() { }, new List() { }); + var rootOrganization = new Organization(rootOrganizationId, "root", new List() { }, new List() { organization }); + var organizer = new Organizer(organizerId); + + + var identityContext = new IdentityContext(Guid.NewGuid().ToString(), "Admin", true, new Dictionary()); + + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + _organizationRepositoryMock.Setup(repo => repo.GetAsync(comand.RootOrganizationId)).ReturnsAsync(rootOrganization); + _organizerRepositoryMock.Setup(repo => repo.GetAsync(comand.OrganizerId)).ReturnsAsync((Organizer)null); + + var cancelationToken = new CancellationToken(); + + // Act & Assert + Func act = async () => await _addOrganizerToOrganizationHandler.HandleAsync(comand, cancelationToken); + await act.Should().ThrowAsync(); + } + } +} diff --git a/MiniSpace.Services.Organizations/tests/MiniSpace.Services.Organizations.Application.UnitTests/Commands/Handlers/CreateOrganizationHandlerTest.cs b/MiniSpace.Services.Organizations/tests/MiniSpace.Services.Organizations.Application.UnitTests/Commands/Handlers/CreateOrganizationHandlerTest.cs new file mode 100644 index 00000000..408c1b7a --- /dev/null +++ b/MiniSpace.Services.Organizations/tests/MiniSpace.Services.Organizations.Application.UnitTests/Commands/Handlers/CreateOrganizationHandlerTest.cs @@ -0,0 +1,159 @@ +using Xunit; +using Moq; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Threading; +using FluentAssertions; +using MiniSpace.Services.Organizations.Application.Commands.Handlers; +using MiniSpace.Services.Organizations.Core.Repositories; +using MiniSpace.Services.Organizations.Application.Services; +using MiniSpace.Services.Organizations.Core.Entities; +using MiniSpace.Services.Organizations.Infrastructure.Contexts; +using MiniSpace.Services.Organizations.Application.Commands; +using MiniSpace.Services.Organizations.Application.Exceptions; + +namespace MiniSpace.Services.Organizations.Application.UnitTests.Commands.Handlers +{ + public class CreateOrganizationHandlerTest + { + private readonly CreateOrganizationHandler _createOrganizationHandler; + private readonly Mock _organizationRepositoryMock; + private readonly Mock _appContextMock; + private readonly Mock _messageBrokerMock; + + public CreateOrganizationHandlerTest() + { + _organizationRepositoryMock = new Mock(); + _appContextMock = new Mock(); + _messageBrokerMock = new Mock(); + _createOrganizationHandler = new CreateOrganizationHandler( + _organizationRepositoryMock.Object, + _appContextMock.Object, + _messageBrokerMock.Object + ); + } + + [Fact] + public async Task HandleAsync_WithValidorganizationAndAuthorised_ShouldUpdateRootOrganization() + { + // Arrange + var rootOrganizationId = Guid.NewGuid(); + var organizationId = Guid.NewGuid(); + var parentId = Guid.NewGuid(); + var comand = new CreateOrganization(organizationId, "this", rootOrganizationId, parentId); + + var organization = new Organization(organizationId, "this"); + var parent = new Organization(parentId, "parent", new List() { }, new List() { }); + var rootOrganization = new Organization(rootOrganizationId, "root", new List() { }, new List() { parent }); + var identityContext = new IdentityContext(Guid.NewGuid().ToString(), "Admin", true, new Dictionary()); + + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + _organizationRepositoryMock.Setup(repo => repo.GetAsync(comand.RootId)).ReturnsAsync(rootOrganization); + + var cancelationToken = new CancellationToken(); + + // Act + await _createOrganizationHandler.HandleAsync(comand, cancelationToken); + + // Assert + _organizationRepositoryMock.Verify(repo => repo.UpdateAsync(rootOrganization), Times.Once()); + } + + [Fact] + public async Task HandleAsync_WithoutAdminRole_ShouldThrowUnauthorizedAccessException() + { + // Arrange + var rootOrganizationId = Guid.NewGuid(); + var organizationId = Guid.NewGuid(); + var parentId = Guid.NewGuid(); + var comand = new CreateOrganization(organizationId, "this", rootOrganizationId, parentId); + + var organization = new Organization(organizationId, "this"); + var parent = new Organization(parentId, "parent", new List() { }, new List() { }); + var rootOrganization = new Organization(rootOrganizationId, "root", new List() { }, new List() { parent }); + var identityContext = new IdentityContext(Guid.NewGuid().ToString(), "", true, new Dictionary()); + + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + _organizationRepositoryMock.Setup(repo => repo.GetAsync(comand.RootId)).ReturnsAsync(rootOrganization); + + var cancelationToken = new CancellationToken(); + + // Act & Assert + Func act = async () => await _createOrganizationHandler.HandleAsync(comand, cancelationToken); + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task HandleAsync_InavalidOrganizationRoot_ShouldThrowRootOrganizationNotFoundException() + { + // Arrange + var rootOrganizationId = Guid.NewGuid(); + var organizationId = Guid.NewGuid(); + var parentId = Guid.NewGuid(); + var comand = new CreateOrganization(organizationId, "this", rootOrganizationId, parentId); + + var organization = new Organization(organizationId, "this"); + var parent = new Organization(parentId, "parent", new List() { }, new List() { }); + var rootOrganization = new Organization(rootOrganizationId, "root", new List() { }, new List() { }); + var identityContext = new IdentityContext(Guid.NewGuid().ToString(), "Admin", true, new Dictionary()); + + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + _organizationRepositoryMock.Setup(repo => repo.GetAsync(comand.RootId)).ReturnsAsync((Organization)null); + + var cancelationToken = new CancellationToken(); + + // Act & Assert + Func act = async () => await _createOrganizationHandler.HandleAsync(comand, cancelationToken); + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task HandleAsync_WithoutOrganizationInOrganizationRoot_ShouldThrowParentOrganizationNotFoundException() + { + // Arrange + var rootOrganizationId = Guid.NewGuid(); + var organizationId = Guid.NewGuid(); + var parentId = Guid.NewGuid(); + var comand = new CreateOrganization(organizationId, "this", rootOrganizationId, parentId); + + var organization = new Organization(organizationId, "this"); + var parent = new Organization(parentId, "parent", new List() { }, new List() { }); + var rootOrganization = new Organization(rootOrganizationId, "root", new List() { }, new List() { }); + var identityContext = new IdentityContext(Guid.NewGuid().ToString(), "Admin", true, new Dictionary()); + + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + _organizationRepositoryMock.Setup(repo => repo.GetAsync(comand.RootId)).ReturnsAsync(rootOrganization); + + var cancelationToken = new CancellationToken(); + + // Act & Assert + Func act = async () => await _createOrganizationHandler.HandleAsync(comand, cancelationToken); + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task HandleAsync_WithInvalidName_ShouldThrowInvalidOrganizationNameException() + { + // Arrange + var rootOrganizationId = Guid.NewGuid(); + var organizationId = Guid.NewGuid(); + var parentId = Guid.NewGuid(); + var comand = new CreateOrganization(organizationId, "", rootOrganizationId, parentId); + + var organization = new Organization(organizationId, "this"); + var parent = new Organization(parentId, "parent", new List() { }, new List() { }); + var rootOrganization = new Organization(rootOrganizationId, "root", new List() { }, new List() { parent }); + var identityContext = new IdentityContext(Guid.NewGuid().ToString(), "Admin", true, new Dictionary()); + + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + _organizationRepositoryMock.Setup(repo => repo.GetAsync(comand.RootId)).ReturnsAsync(rootOrganization); + + var cancelationToken = new CancellationToken(); + + // Act & Assert + Func act = async () => await _createOrganizationHandler.HandleAsync(comand, cancelationToken); + await act.Should().ThrowAsync(); + } + } +} diff --git a/MiniSpace.Services.Organizations/tests/MiniSpace.Services.Organizations.Application.UnitTests/Commands/Handlers/CreateRoorOrganizationHandlerTest.cs b/MiniSpace.Services.Organizations/tests/MiniSpace.Services.Organizations.Application.UnitTests/Commands/Handlers/CreateRoorOrganizationHandlerTest.cs new file mode 100644 index 00000000..6f4c88d1 --- /dev/null +++ b/MiniSpace.Services.Organizations/tests/MiniSpace.Services.Organizations.Application.UnitTests/Commands/Handlers/CreateRoorOrganizationHandlerTest.cs @@ -0,0 +1,95 @@ +using FluentAssertions; +using MiniSpace.Services.Organizations.Application.Commands; +using MiniSpace.Services.Organizations.Application.Commands.Handlers; +using MiniSpace.Services.Organizations.Application.Exceptions; +using MiniSpace.Services.Organizations.Application.Services; +using MiniSpace.Services.Organizations.Core.Entities; +using MiniSpace.Services.Organizations.Core.Repositories; +using MiniSpace.Services.Organizations.Infrastructure.Contexts; +using Moq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace MiniSpace.Services.Organizations.Application.UnitTests.Commands.Handlers +{ + public class CreateRoorOrganizationHandlerTest + { + private readonly CreateRootOrganizationHandler _createRootOrganizationHandler; + private readonly Mock _organizationRepositoryMock; + private readonly Mock _appContextMock; + private readonly Mock _messageBrokerMock; + + public CreateRoorOrganizationHandlerTest() + { + _organizationRepositoryMock = new Mock(); + _appContextMock = new Mock(); + _messageBrokerMock = new Mock(); + _createRootOrganizationHandler = new CreateRootOrganizationHandler( + _organizationRepositoryMock.Object, + _appContextMock.Object, + _messageBrokerMock.Object + ); + } + + [Fact] + public async Task HandleAsync_WithValidOrganizationNameAndAuthorised_ShouldNotThrow() + { + // Arrange + var organizationId = Guid.NewGuid(); + var comand = new CreateRootOrganization(organizationId, "this"); + + var organization = new Organization(organizationId, "this"); + + var identityContext = new IdentityContext(Guid.NewGuid().ToString(), "Admin", true, new Dictionary()); + + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + + var cancelationToken = new CancellationToken(); + + // Act & Assert + Func act = async () => await _createRootOrganizationHandler.HandleAsync(comand, cancelationToken); + await act.Should().NotThrowAsync(); + } + + [Fact] + public async Task HandleAsync_WithoutAdminRole_ShouldThrowUnauthorizedAccessException() + { + // Arrange + var organizationId = Guid.NewGuid(); + var comand = new CreateRootOrganization(organizationId, "this"); + + var identityContext = new IdentityContext(Guid.NewGuid().ToString(), "", true, new Dictionary()); + + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + + var cancelationToken = new CancellationToken(); + + // Act & Assert + Func act = async () => await _createRootOrganizationHandler.HandleAsync(comand, cancelationToken); + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task HandleAsync_InavalidOrganizationName_ShouldThrowInvalidOrganizationNameException() + { + // Arrange + var organizationId = Guid.NewGuid(); + var comand = new CreateRootOrganization(organizationId, ""); + + var identityContext = new IdentityContext(Guid.NewGuid().ToString(), "Admin", true, new Dictionary()); + + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + + var cancelationToken = new CancellationToken(); + + // Act & Assert + Func act = async () => await _createRootOrganizationHandler.HandleAsync(comand, cancelationToken); + await act.Should().ThrowAsync (); + } + } +} diff --git a/MiniSpace.Services.Organizations/tests/MiniSpace.Services.Organizations.Application.UnitTests/Commands/Handlers/RemoveOrganizerFromOrganizationHandlerTest.cs b/MiniSpace.Services.Organizations/tests/MiniSpace.Services.Organizations.Application.UnitTests/Commands/Handlers/RemoveOrganizerFromOrganizationHandlerTest.cs new file mode 100644 index 00000000..598a8ce0 --- /dev/null +++ b/MiniSpace.Services.Organizations/tests/MiniSpace.Services.Organizations.Application.UnitTests/Commands/Handlers/RemoveOrganizerFromOrganizationHandlerTest.cs @@ -0,0 +1,182 @@ +using Xunit; +using Moq; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Threading; +using FluentAssertions; +using MiniSpace.Services.Organizations.Application.Commands.Handlers; +using MiniSpace.Services.Organizations.Core.Repositories; +using MiniSpace.Services.Organizations.Application.Services; +using MiniSpace.Services.Organizations.Core.Entities; +using MiniSpace.Services.Organizations.Infrastructure.Contexts; +using MiniSpace.Services.Organizations.Application.Commands; +using MiniSpace.Services.Organizations.Application.Exceptions; + +namespace MiniSpace.Services.Organizations.Application.UnitTests.Commands.Handlers +{ + public class RemoveOrganizerFromOrganizationHandlerTest + { + private readonly RemoveOrganizerFromOrganizationHandler _removeOrganizerFromOrganizationHandler; + private readonly Mock _organizationRepositoryMock; + private readonly Mock _organizerRepositoryMock; + private readonly Mock _appContextMock; + private readonly Mock _messageBrokerMock; + + public RemoveOrganizerFromOrganizationHandlerTest() + { + _organizationRepositoryMock = new Mock(); + _organizerRepositoryMock = new Mock(); + _appContextMock = new Mock(); + _messageBrokerMock = new Mock(); + _removeOrganizerFromOrganizationHandler = new RemoveOrganizerFromOrganizationHandler( + _organizationRepositoryMock.Object, + _organizerRepositoryMock.Object, + _appContextMock.Object, + _messageBrokerMock.Object + ); + } + + [Fact] + public async Task HandleAsync_WithValidOrganizationAndAuthorised_ShouldUpdateOrganization() + { + // Arrange + var rootOrganizationId = Guid.NewGuid(); + var organizationId = Guid.NewGuid(); + var organizerId = Guid.NewGuid(); + var comand = new RemoveOrganizerFromOrganization(rootOrganizationId, organizationId, organizerId); + + var organizer = new Organizer(organizerId); + var organization = new Organization(organizationId, "this", new List() { organizer }, new List() { }); + var rootOrganization = new Organization(rootOrganizationId, "root", new List() { }, new List() { organization }); + + + + var identityContext = new IdentityContext(Guid.NewGuid().ToString(), "Admin", true, new Dictionary()); + + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + _organizationRepositoryMock.Setup(repo => repo.GetAsync(comand.RootOrganizationId)).ReturnsAsync(rootOrganization); + _organizerRepositoryMock.Setup(repo => repo.GetAsync(comand.OrganizerId)).ReturnsAsync(organizer); + + var cancelationToken = new CancellationToken(); + + // Act + await _removeOrganizerFromOrganizationHandler.HandleAsync(comand, cancelationToken); + + // Assert + _organizationRepositoryMock.Verify(repo => repo.UpdateAsync(rootOrganization), Times.Once()); + } + + [Fact] + public async Task HandleAsync_WithoutAdminRole_ShouldThrowUnauthorizedAccessException() + { + // Arrange + var rootOrganizationId = Guid.NewGuid(); + var organizationId = Guid.NewGuid(); + var organizerId = Guid.NewGuid(); + var comand = new RemoveOrganizerFromOrganization(rootOrganizationId, organizationId, organizerId); + + var organizer = new Organizer(organizerId); + var organization = new Organization(organizationId, "this", new List() { organizer }, new List() { }); + var rootOrganization = new Organization(rootOrganizationId, "root", new List() { }, new List() { organization }); + + + + var identityContext = new IdentityContext(Guid.NewGuid().ToString(), "", true, new Dictionary()); + + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + _organizationRepositoryMock.Setup(repo => repo.GetAsync(comand.RootOrganizationId)).ReturnsAsync(rootOrganization); + _organizerRepositoryMock.Setup(repo => repo.GetAsync(comand.OrganizerId)).ReturnsAsync(organizer); + + var cancelationToken = new CancellationToken(); + + // Act & Assert + Func act = async () => await _removeOrganizerFromOrganizationHandler.HandleAsync(comand, cancelationToken); + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task HandleAsync_InavalidOrganizationRoot_ShouldThrowRootOrganizationNotFoundException() + { + // Arrange + var rootOrganizationId = Guid.NewGuid(); + var organizationId = Guid.NewGuid(); + var organizerId = Guid.NewGuid(); + var comand = new RemoveOrganizerFromOrganization(rootOrganizationId, organizationId, organizerId); + + var organizer = new Organizer(organizerId); + var organization = new Organization(organizationId, "this", new List() { organizer }, new List() { }); + var rootOrganization = new Organization(rootOrganizationId, "root", new List() { }, new List() { organization }); + + + + var identityContext = new IdentityContext(Guid.NewGuid().ToString(), "Admin", true, new Dictionary()); + + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + _organizationRepositoryMock.Setup(repo => repo.GetAsync(comand.RootOrganizationId)).ReturnsAsync((Organization)null); + _organizerRepositoryMock.Setup(repo => repo.GetAsync(comand.OrganizerId)).ReturnsAsync(organizer); + + var cancelationToken = new CancellationToken(); + + // Act & Assert + Func act = async () => await _removeOrganizerFromOrganizationHandler.HandleAsync(comand, cancelationToken); + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task HandleAsync_WithoutOrganizationInOrganizationRoot_ShouldThrowOrganizationNotFoundException() + { + // Arrange + var rootOrganizationId = Guid.NewGuid(); + var organizationId = Guid.NewGuid(); + var organizerId = Guid.NewGuid(); + var comand = new RemoveOrganizerFromOrganization(rootOrganizationId, organizationId, organizerId); + + var organizer = new Organizer(organizerId); + var organization = new Organization(organizationId, "this", new List() { organizer }, new List() { }); + var rootOrganization = new Organization(rootOrganizationId, "root", new List() { }, new List() { }); + + + + var identityContext = new IdentityContext(Guid.NewGuid().ToString(), "Admin", true, new Dictionary()); + + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + _organizationRepositoryMock.Setup(repo => repo.GetAsync(comand.RootOrganizationId)).ReturnsAsync(rootOrganization); + _organizerRepositoryMock.Setup(repo => repo.GetAsync(comand.OrganizerId)).ReturnsAsync(organizer); + + var cancelationToken = new CancellationToken(); + + // Act & Assert + Func act = async () => await _removeOrganizerFromOrganizationHandler.HandleAsync(comand, cancelationToken); + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task HandleAsync_WithInvalidOrganizer_ShouldThrowOrganizerNotFoundException() + { + // Arrange + var rootOrganizationId = Guid.NewGuid(); + var organizationId = Guid.NewGuid(); + var organizerId = Guid.NewGuid(); + var comand = new RemoveOrganizerFromOrganization(rootOrganizationId, organizationId, organizerId); + + var organizer = new Organizer(organizerId); + var organization = new Organization(organizationId, "this", new List() { organizer }, new List() { }); + var rootOrganization = new Organization(rootOrganizationId, "root", new List() { }, new List() { organization }); + + + + var identityContext = new IdentityContext(Guid.NewGuid().ToString(), "Admin", true, new Dictionary()); + + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + _organizationRepositoryMock.Setup(repo => repo.GetAsync(comand.RootOrganizationId)).ReturnsAsync(rootOrganization); + _organizerRepositoryMock.Setup(repo => repo.GetAsync(comand.OrganizerId)).ReturnsAsync((Organizer)null); + + var cancelationToken = new CancellationToken(); + + // Act & Assert + Func act = async () => await _removeOrganizerFromOrganizationHandler.HandleAsync(comand, cancelationToken); + await act.Should().ThrowAsync(); + } + } +} diff --git a/MiniSpace.Services.Organizations/tests/MiniSpace.Services.Organizations.Application.UnitTests/Events/External/Handlers/OrganizerRightRevokedHandlerTest.cs b/MiniSpace.Services.Organizations/tests/MiniSpace.Services.Organizations.Application.UnitTests/Events/External/Handlers/OrganizerRightRevokedHandlerTest.cs new file mode 100644 index 00000000..5b57ce21 --- /dev/null +++ b/MiniSpace.Services.Organizations/tests/MiniSpace.Services.Organizations.Application.UnitTests/Events/External/Handlers/OrganizerRightRevokedHandlerTest.cs @@ -0,0 +1,75 @@ +using Xunit; +using Moq; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Threading; +using FluentAssertions; +using MiniSpace.Services.Organizations.Application.Commands.Handlers; +using MiniSpace.Services.Organizations.Core.Repositories; +using MiniSpace.Services.Organizations.Application.Services; +using MiniSpace.Services.Organizations.Core.Entities; +using MiniSpace.Services.Organizations.Infrastructure.Contexts; +using MiniSpace.Services.Organizations.Application.Commands; +using MiniSpace.Services.Organizations.Application.Exceptions; +using MiniSpace.Services.Organizations.Application.Events.External.Handlers; +using MiniSpace.Services.Organizations.Application.Events.External; + +namespace MiniSpace.Services.Organizations.Application.UnitTests.Events.External.Handlers +{ + public class OrganizerRightRevokedHandlerTest + { + private readonly OrganizerRightsRevokedHandler _organizerRightsRevokedHandler; + private readonly Mock _organizerRepository; + private readonly Mock _organizationRepository; + + public OrganizerRightRevokedHandlerTest() + { + _organizerRepository = new Mock(); + _organizationRepository = new Mock(); + _organizerRightsRevokedHandler = new OrganizerRightsRevokedHandler(_organizerRepository.Object, _organizationRepository.Object); + } + + [Fact] + public async Task HandleAsync_WithValidOrganizer_ShouldNotThrow() + { + // Arrange + var userId = Guid.NewGuid(); + var @event = new OrganizerRightsRevoked(userId); + var organizer = new Organizer(userId); + var organization = new Organization(Guid.NewGuid(), "name", new List() { organizer }, new List() { }); + + _organizerRepository.Setup(repo => repo.GetAsync(@event.UserId)).ReturnsAsync(organizer); + _organizationRepository.Setup(repo => repo.GetOrganizerOrganizationsAsync(@event.UserId)).ReturnsAsync(new List() { organization }); + + var cancelationToken = new CancellationToken(); + + // Act + await _organizerRightsRevokedHandler.HandleAsync(@event, cancelationToken); + + // Arrange + _organizationRepository.Verify(repo => repo.UpdateAsync(organization), Times.Once()); + _organizerRepository.Verify(repo => repo.DeleteAsync(userId), Times.Once()); + } + + [Fact] + public async Task HandleAsync_WithInvalidOrganizer_ShouldThrowOrganizerNotFoundException() + { + // Arrange + var userId = Guid.NewGuid(); + var @event = new OrganizerRightsRevoked(userId); + var organizer = new Organizer(userId); + var organization = new Organization(Guid.NewGuid(), "name", new List() { organizer }, new List() { }); + + _organizerRepository.Setup(repo => repo.GetAsync(@event.UserId)).ReturnsAsync((Organizer)null); + _organizationRepository.Setup(repo => repo.GetOrganizerOrganizationsAsync(@event.UserId)).ReturnsAsync(new List() { organization }); + + var cancelationToken = new CancellationToken(); + + // Act & Assert + Func act = async () => await _organizerRightsRevokedHandler.HandleAsync(@event, cancelationToken); + await act.Should().ThrowAsync(); + } + + } +} diff --git a/MiniSpace.Services.Organizations/tests/MiniSpace.Services.Organizations.Application.UnitTests/Events/External/Handlers/OrganizerRightsGrantedHandlerTest.cs b/MiniSpace.Services.Organizations/tests/MiniSpace.Services.Organizations.Application.UnitTests/Events/External/Handlers/OrganizerRightsGrantedHandlerTest.cs new file mode 100644 index 00000000..7cf7ed76 --- /dev/null +++ b/MiniSpace.Services.Organizations/tests/MiniSpace.Services.Organizations.Application.UnitTests/Events/External/Handlers/OrganizerRightsGrantedHandlerTest.cs @@ -0,0 +1,63 @@ +using Xunit; +using Moq; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Threading; +using FluentAssertions; +using MiniSpace.Services.Organizations.Application.Commands.Handlers; +using MiniSpace.Services.Organizations.Core.Repositories; +using MiniSpace.Services.Organizations.Application.Services; +using MiniSpace.Services.Organizations.Core.Entities; +using MiniSpace.Services.Organizations.Infrastructure.Contexts; +using MiniSpace.Services.Organizations.Application.Commands; +using MiniSpace.Services.Organizations.Application.Exceptions; +using MiniSpace.Services.Organizations.Application.Events.External.Handlers; +using MiniSpace.Services.Organizations.Application.Events.External; + +namespace MiniSpace.Services.Organizations.Application.UnitTests.Events.External.Handlers +{ + public class OrganizerRightsGrantedHandlerTest + { + private readonly OrganizerRightsGrantedHandler _organizerRightsGrantedHandler; + private readonly Mock _organizerRepository; + + public OrganizerRightsGrantedHandlerTest() + { + _organizerRepository = new Mock(); + _organizerRightsGrantedHandler = new OrganizerRightsGrantedHandler(_organizerRepository.Object); + } + + [Fact] + public async Task HandleAsync_WithValidOrganizer_ShouldNotThrow() + { + // Arrange + var userId = Guid.NewGuid(); + var @event = new OrganizerRightsGranted(userId); + + _organizerRepository.Setup(repo => repo.ExistsAsync(userId)).ReturnsAsync(false); + + var cancelationToken = new CancellationToken(); + + // Act & Assert + Func act = async () => await _organizerRightsGrantedHandler.HandleAsync(@event, cancelationToken); + await act.Should().NotThrowAsync(); + } + + [Fact] + public async Task HandleAsync_WithInvalidOrganizer_ShouldThrowOrganizerAlreadyAddedException() + { + // Arrange + var userId = Guid.NewGuid(); + var @event = new OrganizerRightsGranted(userId); + + _organizerRepository.Setup(repo => repo.ExistsAsync(userId)).ReturnsAsync(true); + + var cancelationToken = new CancellationToken(); + + // Act & Assert + Func act = async () => await _organizerRightsGrantedHandler.HandleAsync(@event, cancelationToken); + await act.Should().ThrowAsync(); + } + } +} diff --git a/MiniSpace.Services.Organizations/tests/MiniSpace.Services.Organizations.Application.UnitTests/MiniSpace.Services.Organizations.Application.UnitTests.csproj b/MiniSpace.Services.Organizations/tests/MiniSpace.Services.Organizations.Application.UnitTests/MiniSpace.Services.Organizations.Application.UnitTests.csproj new file mode 100644 index 00000000..a38b990f --- /dev/null +++ b/MiniSpace.Services.Organizations/tests/MiniSpace.Services.Organizations.Application.UnitTests/MiniSpace.Services.Organizations.Application.UnitTests.csproj @@ -0,0 +1,32 @@ + + + + net8.0 + disable + + false + true + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + diff --git a/MiniSpace.Services.Organizations/tests/MiniSpace.Services.Organizations.Core.UnitTests/Entities/AggregateIdTest.cs b/MiniSpace.Services.Organizations/tests/MiniSpace.Services.Organizations.Core.UnitTests/Entities/AggregateIdTest.cs new file mode 100644 index 00000000..846e0833 --- /dev/null +++ b/MiniSpace.Services.Organizations/tests/MiniSpace.Services.Organizations.Core.UnitTests/Entities/AggregateIdTest.cs @@ -0,0 +1,38 @@ +using MiniSpace.Services.Organizations.Core.Entities; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace MiniSpace.Services.Organizations.Core.UnitTests.Entities +{ + public class AggregateIdTest + { + [Fact] + public void AggregateId_CreatedTwice_ShuldBeDiffrent() + { + // Arrange & Act + var id1 = new AggregateId(); + var id2 = new AggregateId(); + + // Assert + Assert.NotEqual(id1.Value, id2.Value); + } + + [Fact] + public void AggregateId_CreatedTwiceSameGuid_ShuldBeSame() + { + // Arrange + var id = Guid.NewGuid(); + + // Act + var id1 = new AggregateId(id); + var id2 = new AggregateId(id); + + // Assert + Assert.Equal(id1.Value, id2.Value); + } + } +} diff --git a/MiniSpace.Services.Organizations/tests/MiniSpace.Services.Organizations.Core.UnitTests/Entities/OrganizationTest.cs b/MiniSpace.Services.Organizations/tests/MiniSpace.Services.Organizations.Core.UnitTests/Entities/OrganizationTest.cs new file mode 100644 index 00000000..0e6c30a6 --- /dev/null +++ b/MiniSpace.Services.Organizations/tests/MiniSpace.Services.Organizations.Core.UnitTests/Entities/OrganizationTest.cs @@ -0,0 +1,176 @@ +using MiniSpace.Services.Organizations.Core.Entities; +using MiniSpace.Services.Organizations.Core.Exceptions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace MiniSpace.Services.Organizations.Core.UnitTests.Entities +{ + public class OrganizationTest + { + [Fact] + public void AddOrganizer_OrganizorAdded_ShouldAddOrgizer() + { + // Arrange + var organizerId = Guid.NewGuid(); + var organiazation = new Organization(Guid.NewGuid(), "name"); + + // Act + organiazation.AddOrganizer(organizerId); + + // Assert + Assert.True(organiazation.Organizers.Any(x => x.Id == organizerId)); + } + + [Fact] + public void AddOrganizer_OrganizorOlreadyAdded_ShouldThrowOrganizerAlreadyAddedToOrganizationException() + { + // Arrange + var organizerId = Guid.NewGuid(); + var organiazation = new Organization(Guid.NewGuid(), "name"); + organiazation.AddOrganizer(organizerId); + + // Act & Assert + var act = new Action(() => organiazation.AddOrganizer(organizerId)); + Assert.Throws(act); + } + + [Fact] + public void RemoveOrganizer_OrganizorRemoved_ShouldRemoveOrganizer() + { + // Arrange + var organizerId = Guid.NewGuid(); + var organiazation = new Organization(Guid.NewGuid(), "name"); + organiazation.AddOrganizer(organizerId); + + // Act + organiazation.RemoveOrganizer(organizerId); + + // Assert + Assert.DoesNotContain(new Organizer(organizerId), organiazation.Organizers); + } + + [Fact] + public void RemoveOrganizer_OrganizorOlreadyRemoved_ShouldThrowOrganizerIsNotInOrganization() + { + // Arrange + var organizerId = Guid.NewGuid(); + var organiazation = new Organization(Guid.NewGuid(), "name"); + + // Act & Assert + var act = new Action(() => organiazation.RemoveOrganizer(organizerId)); + Assert.Throws(act); + } + + [Fact] + public void AddSubOrganization_AddOrganization_ShouldAddOrganization() + { + // Arrange + var organizationId = Guid.NewGuid(); + var organiazation = new Organization(organizationId, "name"); + var neworganization = new Organization(Guid.NewGuid(), "name"); + + // Act + organiazation.AddSubOrganization(neworganization); + + // Assert + Assert.Contains(neworganization, organiazation.SubOrganizations); + + } + + + [Fact] + public void GetSubOrganization_AskedAboutThemself_ShouldReturnThemself() + { + // Arrange + var organizationId = Guid.NewGuid(); + var organiazation = new Organization(organizationId, "name"); + + // Act + var result = organiazation.GetSubOrganization(organizationId); + + // Assert + Assert.Equal(result.Id.ToString(), organizationId.ToString()); + + } + + [Fact] + public void GetSubOrganization_DorsNotContainsOrganization_ShouldReturnNull() + { + // Arrange + var organizationId = Guid.NewGuid(); + var organiazation = new Organization(Guid.NewGuid(), "name"); + + // Act + var result = organiazation.GetSubOrganization(organizationId); + + // Assert + Assert.Null(result); + + } + + [Fact] + public void GetSubOrganization_ContainsOrganization_ShouldReturnOrganization() + { + // Arrange + var organizationId = Guid.NewGuid(); + var organiazation = new Organization(Guid.NewGuid(), "name"); + organiazation.AddSubOrganization(new Organization(organizationId, "name")); + + // Act + var result = organiazation.GetSubOrganization(organizationId); + + // Assert + Assert.Equal(result.Id.ToString(), organizationId.ToString()); + + } + + [Fact] + public void FindOrganizations_FindOrganizationOfAOrganizer_ShouldReturnAllOrganizerOrganizations() + { + // Arrange + var organizerId = Guid.NewGuid(); + var organizer = new Organizer(organizerId); + var organizers = new List() { organizer }; + var organizers2 = new List() { }; + var ch2_1 = new Organization(Guid.NewGuid(), "ch2_1", organizers, new List { }); + var ch1_1 = new Organization(Guid.NewGuid(), "ch1_1", organizers, new List { }); + var ch1_2 = new Organization(Guid.NewGuid(), "ch1_2", organizers2, new List { ch2_1}); + var root = new Organization(Guid.NewGuid(), "root", organizers, new List { ch1_1, ch1_2}); + + //Act + var result = Organization.FindOrganizations(organizerId, root); + + // Assert + Assert.Contains(root, result); + Assert.Contains(ch1_1, result); + Assert.DoesNotContain(ch1_2, result); + Assert.Contains(ch2_1, result); + } + + [Fact] + public void FindAllChildrenOrganizations_FromRoot_ShouldReturnFullOrganizationTree() + { + // Arrange + var organizerId = Guid.NewGuid(); + var organizer = new Organizer(organizerId); + var organizers = new List() { organizer }; + var ch2_1 = new Organization(Guid.NewGuid(), "ch2_1", organizers, new List { }); + var ch1_1 = new Organization(Guid.NewGuid(), "ch1_1", organizers, new List { }); + var ch1_2 = new Organization(Guid.NewGuid(), "ch1_1", organizers, new List { ch2_1 }); + var root = new Organization(Guid.NewGuid(), "root", organizers, new List { ch1_1, ch1_2 }); + + //Act + var result = Organization.FindOrganizations(organizerId, root); + + // Assert + Assert.Contains(root, result); + Assert.Contains(ch1_1, result); + Assert.Contains(ch1_2, result); + Assert.Contains(ch1_2, result); + } + } +} diff --git a/MiniSpace.Services.Organizations/tests/MiniSpace.Services.Organizations.Core.UnitTests/MiniSpace.Services.Organizations.Core.UnitTests.csproj b/MiniSpace.Services.Organizations/tests/MiniSpace.Services.Organizations.Core.UnitTests/MiniSpace.Services.Organizations.Core.UnitTests.csproj new file mode 100644 index 00000000..a38b990f --- /dev/null +++ b/MiniSpace.Services.Organizations/tests/MiniSpace.Services.Organizations.Core.UnitTests/MiniSpace.Services.Organizations.Core.UnitTests.csproj @@ -0,0 +1,32 @@ + + + + net8.0 + disable + + false + true + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + diff --git a/MiniSpace.Services.Posts/.gitignore b/MiniSpace.Services.Posts/.gitignore index c1aafb51..fffd35c7 100644 --- a/MiniSpace.Services.Posts/.gitignore +++ b/MiniSpace.Services.Posts/.gitignore @@ -3,6 +3,9 @@ ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore +# VSCode +.vscode/ + # User-specific files *.suo *.user diff --git a/MiniSpace.Services.Posts/LICENSE b/MiniSpace.Services.Posts/LICENSE new file mode 100644 index 00000000..b7ea7f0c --- /dev/null +++ b/MiniSpace.Services.Posts/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 DevMentors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MiniSpace.Services.Posts/MiniSpace.Services.Posts.sln b/MiniSpace.Services.Posts/MiniSpace.Services.Posts.sln index 468fc6e9..031553aa 100644 --- a/MiniSpace.Services.Posts/MiniSpace.Services.Posts.sln +++ b/MiniSpace.Services.Posts/MiniSpace.Services.Posts.sln @@ -13,6 +13,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.Posts.Co EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.Posts.Infrastructure", "src\MiniSpace.Services.Posts.Infrastructure\MiniSpace.Services.Posts.Infrastructure.csproj", "{515F6D6B-FB63-40F7-8173-2B94031F29B1}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.Posts.Application.UnitTests", "tests\MiniSpace.Services.Posts.Application.UnitTests\MiniSpace.Services.Posts.Application.UnitTests.csproj", "{63DF71AF-1D31-4A4D-8127-9B8EB359889D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.Posts.Core.UnitTests", "tests\MiniSpace.Services.Posts.Core.UnitTests\MiniSpace.Services.Posts.Core.UnitTests.csproj", "{38556B4C-90E1-453F-8BA7-C0699577A521}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.Posts.Infrastructure.UnitTests", "tests\MiniSpace.Services.Posts.Infrastructure.UnitTests\MiniSpace.Services.Posts.Infrastructure.UnitTests.csproj", "{641E0BF5-8BAA-4ECC-9EA7-18CA3D763166}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -38,11 +44,26 @@ Global {515F6D6B-FB63-40F7-8173-2B94031F29B1}.Debug|Any CPU.Build.0 = Debug|Any CPU {515F6D6B-FB63-40F7-8173-2B94031F29B1}.Release|Any CPU.ActiveCfg = Release|Any CPU {515F6D6B-FB63-40F7-8173-2B94031F29B1}.Release|Any CPU.Build.0 = Release|Any CPU + {63DF71AF-1D31-4A4D-8127-9B8EB359889D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {63DF71AF-1D31-4A4D-8127-9B8EB359889D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {63DF71AF-1D31-4A4D-8127-9B8EB359889D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {63DF71AF-1D31-4A4D-8127-9B8EB359889D}.Release|Any CPU.Build.0 = Release|Any CPU + {38556B4C-90E1-453F-8BA7-C0699577A521}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {38556B4C-90E1-453F-8BA7-C0699577A521}.Debug|Any CPU.Build.0 = Debug|Any CPU + {38556B4C-90E1-453F-8BA7-C0699577A521}.Release|Any CPU.ActiveCfg = Release|Any CPU + {38556B4C-90E1-453F-8BA7-C0699577A521}.Release|Any CPU.Build.0 = Release|Any CPU + {641E0BF5-8BAA-4ECC-9EA7-18CA3D763166}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {641E0BF5-8BAA-4ECC-9EA7-18CA3D763166}.Debug|Any CPU.Build.0 = Debug|Any CPU + {641E0BF5-8BAA-4ECC-9EA7-18CA3D763166}.Release|Any CPU.ActiveCfg = Release|Any CPU + {641E0BF5-8BAA-4ECC-9EA7-18CA3D763166}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {5D62C437-6137-4000-A90A-6549835F593B} = {16C8A8E2-B637-4A8B-828F-876523BCC79F} {A17406AF-E11D-421B-9913-9512BB9B3333} = {16C8A8E2-B637-4A8B-828F-876523BCC79F} {0FD8C55D-00F5-4059-889A-D223C7D1275C} = {16C8A8E2-B637-4A8B-828F-876523BCC79F} {515F6D6B-FB63-40F7-8173-2B94031F29B1} = {16C8A8E2-B637-4A8B-828F-876523BCC79F} + {63DF71AF-1D31-4A4D-8127-9B8EB359889D} = {16C8A8E2-B637-4A8B-828F-876523BCC79F} + {38556B4C-90E1-453F-8BA7-C0699577A521} = {16C8A8E2-B637-4A8B-828F-876523BCC79F} + {641E0BF5-8BAA-4ECC-9EA7-18CA3D763166} = {16C8A8E2-B637-4A8B-828F-876523BCC79F} EndGlobalSection EndGlobal diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Api/Program.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Api/Program.cs index bdbec8ae..cc5bf0b7 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Api/Program.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Api/Program.cs @@ -1,4 +1,6 @@ +using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using Convey; using Convey.Logging; @@ -17,6 +19,7 @@ namespace MiniSpace.Services.Posts.Api { + [ExcludeFromCodeCoverage] public class Program { public static async Task Main(string[] args) diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Dto/PostDto.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Dto/PostDto.cs index cfce86ba..d99a1238 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Dto/PostDto.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Dto/PostDto.cs @@ -1,5 +1,8 @@ +using System.Diagnostics.CodeAnalysis; + namespace MiniSpace.Services.Posts.Application.Dto { + [ExcludeFromCodeCoverage] public class PostDto { public Guid Id { get; set; } diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/Rejected/ChangePostStateRejected.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/Rejected/ChangePostStateRejected.cs index 8415fb2d..b7db4322 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/Rejected/ChangePostStateRejected.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/Rejected/ChangePostStateRejected.cs @@ -1,7 +1,9 @@ +using System.Diagnostics.CodeAnalysis; using Convey.CQRS.Events; namespace MiniSpace.Services.Posts.Application.Events.Rejected { + [ExcludeFromCodeCoverage] public class ChangePostStateRejected : IRejectedEvent { public Guid PostId { get; } diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/Rejected/CreatePostRejected.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/Rejected/CreatePostRejected.cs index 6bbd8a7e..198e1f32 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/Rejected/CreatePostRejected.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/Rejected/CreatePostRejected.cs @@ -1,7 +1,9 @@ +using System.Diagnostics.CodeAnalysis; using Convey.CQRS.Events; namespace MiniSpace.Services.Posts.Application.Events.Rejected { + [ExcludeFromCodeCoverage] public class CreatePostRejected : IRejectedEvent { public Guid PostId { get; } diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/Rejected/DeletePostRejected.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/Rejected/DeletePostRejected.cs index 93c3b9c6..b4e5879f 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/Rejected/DeletePostRejected.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/Rejected/DeletePostRejected.cs @@ -1,7 +1,9 @@ +using System.Diagnostics.CodeAnalysis; using Convey.CQRS.Events; namespace MiniSpace.Services.Posts.Application.Events.Rejected { + [ExcludeFromCodeCoverage] public class DeletePostRejected : IRejectedEvent { public Guid PostId { get; } diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/Rejected/UpdatePostRejected.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/Rejected/UpdatePostRejected.cs index f3638136..883e2d6c 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/Rejected/UpdatePostRejected.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/Rejected/UpdatePostRejected.cs @@ -1,7 +1,9 @@ +using System.Diagnostics.CodeAnalysis; using Convey.CQRS.Events; namespace MiniSpace.Services.Posts.Application.Events.Rejected { + [ExcludeFromCodeCoverage] public class UpdatePostRejected : IRejectedEvent { public Guid PostId { get; } diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Extensions.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Extensions.cs index bca12230..e75cdd89 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Extensions.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Extensions.cs @@ -1,9 +1,11 @@ +using System.Diagnostics.CodeAnalysis; using Convey; using Convey.CQRS.Commands; using Convey.CQRS.Events; namespace MiniSpace.Services.Posts.Application { + [ExcludeFromCodeCoverage] public static class Extensions { public static IConveyBuilder AddApplication(this IConveyBuilder builder) diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Queries/GetOrganizerPosts.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Queries/GetOrganizerPosts.cs index 480b6361..468acc48 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Queries/GetOrganizerPosts.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Queries/GetOrganizerPosts.cs @@ -1,9 +1,11 @@ using System.Collections; +using System.Diagnostics.CodeAnalysis; using Convey.CQRS.Queries; using MiniSpace.Services.Posts.Application.Dto; namespace MiniSpace.Services.Posts.Application.Queries { + [ExcludeFromCodeCoverage] public class GetOrganizerPosts : IQuery> { public Guid OrganizerId { get; set; } diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Queries/GetPost.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Queries/GetPost.cs index 3524d49c..fde0324d 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Queries/GetPost.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Queries/GetPost.cs @@ -1,8 +1,10 @@ +using System.Diagnostics.CodeAnalysis; using Convey.CQRS.Queries; using MiniSpace.Services.Posts.Application.Dto; namespace MiniSpace.Services.Posts.Application.Queries { + [ExcludeFromCodeCoverage] public class GetPost : IQuery { public Guid PostId { get; set; } diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Queries/GetPosts.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Queries/GetPosts.cs index 1efeae6f..a06a7d95 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Queries/GetPosts.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Queries/GetPosts.cs @@ -1,8 +1,10 @@ +using System.Diagnostics.CodeAnalysis; using Convey.CQRS.Queries; using MiniSpace.Services.Posts.Application.Dto; namespace MiniSpace.Services.Posts.Application.Queries { + [ExcludeFromCodeCoverage] public class GetPosts : IQuery> { public Guid EventId { get; set; } diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Contexts/AppContext.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Contexts/AppContext.cs index 62d23d59..958479ae 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Contexts/AppContext.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Contexts/AppContext.cs @@ -1,7 +1,9 @@ +using System.Diagnostics.CodeAnalysis; using MiniSpace.Services.Posts.Application; namespace MiniSpace.Services.Posts.Infrastructure.Contexts { + [ExcludeFromCodeCoverage] internal class AppContext : IAppContext { public string RequestId { get; } diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Contexts/AppContextFactory.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Contexts/AppContextFactory.cs index ac0ff864..65b8aed0 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Contexts/AppContextFactory.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Contexts/AppContextFactory.cs @@ -2,9 +2,11 @@ using Microsoft.AspNetCore.Http; using Newtonsoft.Json; using MiniSpace.Services.Posts.Application; +using System.Diagnostics.CodeAnalysis; namespace MiniSpace.Services.Posts.Infrastructure.Contexts { + [ExcludeFromCodeCoverage] internal sealed class AppContextFactory : IAppContextFactory { private readonly ICorrelationContextAccessor _contextAccessor; diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Contexts/CorrelationContext.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Contexts/CorrelationContext.cs index 191f9e53..23753504 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Contexts/CorrelationContext.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Contexts/CorrelationContext.cs @@ -1,5 +1,8 @@ +using System.Diagnostics.CodeAnalysis; + namespace MiniSpace.Services.Posts.Infrastructure.Contexts { + [ExcludeFromCodeCoverage] internal class CorrelationContext { public string CorrelationId { get; set; } diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Contexts/IdentityContext.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Contexts/IdentityContext.cs index eed550c5..9d615dc0 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Contexts/IdentityContext.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Contexts/IdentityContext.cs @@ -1,5 +1,8 @@ using MiniSpace.Services.Posts.Application; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +[assembly: InternalsVisibleTo("MiniSpace.Services.Posts.Application.UnitTests")] namespace MiniSpace.Services.Posts.Infrastructure.Contexts { internal class IdentityContext : IIdentityContext diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Decorators/OutboxCommandHandlerDecorator.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Decorators/OutboxCommandHandlerDecorator.cs index fe841180..240cacbc 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Decorators/OutboxCommandHandlerDecorator.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Decorators/OutboxCommandHandlerDecorator.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Convey.CQRS.Commands; using Convey.MessageBrokers; using Convey.MessageBrokers.Outbox; @@ -5,6 +6,7 @@ namespace MiniSpace.Services.Posts.Infrastructure.Decorators { + [ExcludeFromCodeCoverage] [Decorator] internal sealed class OutboxCommandHandlerDecorator : ICommandHandler where TCommand : class, ICommand diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Decorators/OutboxEventHandlerDecorator.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Decorators/OutboxEventHandlerDecorator.cs index f98c19ea..ac562b6e 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Decorators/OutboxEventHandlerDecorator.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Decorators/OutboxEventHandlerDecorator.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Convey.CQRS.Events; using Convey.MessageBrokers; using Convey.MessageBrokers.Outbox; @@ -5,6 +6,7 @@ namespace MiniSpace.Services.Posts.Infrastructure.Decorators { + [ExcludeFromCodeCoverage] [Decorator] internal sealed class OutboxEventHandlerDecorator : IEventHandler where TEvent : class, IEvent diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Exceptions/ExceptionToMessageMapper.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Exceptions/ExceptionToMessageMapper.cs index 21453a8b..caa07a45 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Exceptions/ExceptionToMessageMapper.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Exceptions/ExceptionToMessageMapper.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Convey.MessageBrokers.RabbitMQ; using MiniSpace.Services.Posts.Application.Commands; using MiniSpace.Services.Posts.Application.Events.Rejected; @@ -6,6 +7,7 @@ namespace MiniSpace.Services.Posts.Infrastructure.Exceptions { + [ExcludeFromCodeCoverage] internal sealed class ExceptionToMessageMapper : IExceptionToMessageMapper { public object Map(Exception exception, object message) diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Exceptions/ExceptionToResponseMapper.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Exceptions/ExceptionToResponseMapper.cs index 70c051e7..42e3230d 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Exceptions/ExceptionToResponseMapper.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Exceptions/ExceptionToResponseMapper.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; using System.Net; using Convey; using Convey.WebApi.Exceptions; @@ -7,6 +8,7 @@ namespace MiniSpace.Services.Posts.Infrastructure.Exceptions { + [ExcludeFromCodeCoverage] internal sealed class ExceptionToResponseMapper : IExceptionToResponseMapper { private static readonly ConcurrentDictionary Codes = new ConcurrentDictionary(); diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Extensions.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Extensions.cs index c5e400ec..da4d595b 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Extensions.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Extensions.cs @@ -39,9 +39,11 @@ using MiniSpace.Services.Posts.Infrastructure.Mongo.Repositories; using MiniSpace.Services.Posts.Infrastructure.Services; using MiniSpace.Services.Posts.Infrastructure.Services.Workers; +using System.Diagnostics.CodeAnalysis; namespace MiniSpace.Services.Posts.Infrastructure { + [ExcludeFromCodeCoverage] public static class Extensions { public static IConveyBuilder AddInfrastructure(this IConveyBuilder builder) diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Logging/Extensions.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Logging/Extensions.cs index 5e5cd3f9..a1d2c95a 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Logging/Extensions.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Logging/Extensions.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Convey; using Convey.Logging.CQRS; using Microsoft.Extensions.DependencyInjection; @@ -5,6 +6,7 @@ namespace MiniSpace.Services.Posts.Infrastructure.Logging { + [ExcludeFromCodeCoverage] internal static class Extensions { public static IConveyBuilder AddHandlersLogging(this IConveyBuilder builder) diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Logging/MessageToLogTemplateMapper.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Logging/MessageToLogTemplateMapper.cs index f060a7f4..a3aa98da 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Logging/MessageToLogTemplateMapper.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Logging/MessageToLogTemplateMapper.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Convey.Logging.CQRS; using MiniSpace.Services.Posts.Application.Commands; using MiniSpace.Services.Posts.Application.Events; @@ -5,6 +6,7 @@ namespace MiniSpace.Services.Posts.Infrastructure.Logging { + [ExcludeFromCodeCoverage] internal sealed class MessageToLogTemplateMapper : IMessageToLogTemplateMapper { private static IReadOnlyDictionary MessageTemplates diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/EventDocument.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/EventDocument.cs index 47b8b3b0..be59fb7b 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/EventDocument.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/EventDocument.cs @@ -1,7 +1,9 @@ +using System.Diagnostics.CodeAnalysis; using Convey.Types; namespace MiniSpace.Services.Posts.Infrastructure.Mongo.Documents { + [ExcludeFromCodeCoverage] public class EventDocument : IIdentifiable { public Guid Id { get; set; } diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/Extensions.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/Extensions.cs index 86c7f63d..11e99a7b 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/Extensions.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/Extensions.cs @@ -1,8 +1,10 @@ +using System.Diagnostics.CodeAnalysis; using MiniSpace.Services.Posts.Application.Dto; using MiniSpace.Services.Posts.Core.Entities; namespace MiniSpace.Services.Posts.Infrastructure.Mongo.Documents { + [ExcludeFromCodeCoverage] public static class Extensions { public static Post AsEntity(this PostDocument document) diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/PostDocument.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/PostDocument.cs index 7ca45ee4..64fae735 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/PostDocument.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/PostDocument.cs @@ -1,8 +1,10 @@ +using System.Diagnostics.CodeAnalysis; using Convey.Types; using MiniSpace.Services.Posts.Core.Entities; namespace MiniSpace.Services.Posts.Infrastructure.Mongo.Documents { + [ExcludeFromCodeCoverage] public class PostDocument : IIdentifiable { public Guid Id { get; set; } diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Queries/Handlers/GetOrganizerPostsHandler.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Queries/Handlers/GetOrganizerPostsHandler.cs index 4f84c7cb..6ccc9cb5 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Queries/Handlers/GetOrganizerPostsHandler.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Queries/Handlers/GetOrganizerPostsHandler.cs @@ -1,4 +1,5 @@ -using Convey.CQRS.Queries; +using System.Diagnostics.CodeAnalysis; +using Convey.CQRS.Queries; using Convey.Persistence.MongoDB; using MiniSpace.Services.Posts.Application; using MiniSpace.Services.Posts.Application.Dto; @@ -9,6 +10,7 @@ namespace MiniSpace.Services.Posts.Infrastructure.Mongo.Queries.Handlers { + [ExcludeFromCodeCoverage] public class GetOrganizerPostsHandler : IQueryHandler> { private readonly IMongoRepository _postRepository; diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Queries/Handlers/GetPostHandler.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Queries/Handlers/GetPostHandler.cs index 4a6447b5..4e88709a 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Queries/Handlers/GetPostHandler.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Queries/Handlers/GetPostHandler.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Convey.CQRS.Queries; using Convey.Persistence.MongoDB; using MiniSpace.Services.Posts.Application.Dto; @@ -6,6 +7,7 @@ namespace MiniSpace.Services.Posts.Infrastructure.Mongo.Queries.Handlers { + [ExcludeFromCodeCoverage] public class GetPostHandler : IQueryHandler { private readonly IMongoRepository _repository; diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Queries/Handlers/GetPostsHandler.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Queries/Handlers/GetPostsHandler.cs index b75fa383..38085f67 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Queries/Handlers/GetPostsHandler.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Queries/Handlers/GetPostsHandler.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Convey.CQRS.Queries; using Convey.Persistence.MongoDB; using MiniSpace.Services.Posts.Application.Dto; @@ -9,6 +10,7 @@ namespace MiniSpace.Services.Posts.Infrastructure.Mongo.Queries.Handlers { + [ExcludeFromCodeCoverage] public class GetPostsHandler : IQueryHandler> { private readonly IMongoRepository _postRepository; diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Repositories/EventMongoRepository.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Repositories/EventMongoRepository.cs index 687e76e5..d98f1633 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Repositories/EventMongoRepository.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Repositories/EventMongoRepository.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Convey.Persistence.MongoDB; using MiniSpace.Services.Posts.Core.Entities; using MiniSpace.Services.Posts.Core.Repositories; @@ -5,6 +6,7 @@ namespace MiniSpace.Services.Posts.Infrastructure.Mongo.Repositories { + [ExcludeFromCodeCoverage] public class EventMongoRepository : IEventRepository { private readonly IMongoRepository _repository; diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Repositories/PostMongoRepository.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Repositories/PostMongoRepository.cs index 7d6985b1..17f9eb80 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Repositories/PostMongoRepository.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Repositories/PostMongoRepository.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Convey.Persistence.MongoDB; using MiniSpace.Services.Posts.Core.Entities; using MiniSpace.Services.Posts.Core.Repositories; @@ -7,6 +8,7 @@ namespace MiniSpace.Services.Posts.Infrastructure.Mongo.Repositories { + [ExcludeFromCodeCoverage] public class PostMongoRepository : IPostRepository { private readonly IMongoRepository _repository; diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Services/MessageBroker.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Services/MessageBroker.cs index 83b38824..b4ed6776 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Services/MessageBroker.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Services/MessageBroker.cs @@ -6,7 +6,9 @@ using Microsoft.Extensions.Logging; using OpenTracing; using MiniSpace.Services.Posts.Application.Services; +using System.Runtime.CompilerServices; +[assembly: InternalsVisibleTo("MiniSpace.Services.Posts.Infrastructure.UnitTests")] namespace MiniSpace.Services.Posts.Infrastructure.Services { internal sealed class MessageBroker : IMessageBroker diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Services/Workers/PostStateUpdaterWorker.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Services/Workers/PostStateUpdaterWorker.cs index 9ec66424..f0d0b579 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Services/Workers/PostStateUpdaterWorker.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Services/Workers/PostStateUpdaterWorker.cs @@ -11,7 +11,7 @@ public class PostStateUpdaterWorker: BackgroundService private readonly IMessageBroker _messageBroker; private readonly ICommandDispatcher _commandDispatcher; private readonly IDateTimeProvider _dateTimeProvider; - private const int MinutesInterval = 5; + public const int MinutesInterval = 5; public PostStateUpdaterWorker(IMessageBroker messageBroker, ICommandDispatcher commandDispatcher, IDateTimeProvider dateTimeProvider) diff --git a/MiniSpace.Services.Posts/tests/MiniSpace.Services.Posts.Application.UnitTests/Commands/Handlers/ChangePostStateHandlerTest.cs b/MiniSpace.Services.Posts/tests/MiniSpace.Services.Posts.Application.UnitTests/Commands/Handlers/ChangePostStateHandlerTest.cs new file mode 100644 index 00000000..527b9378 --- /dev/null +++ b/MiniSpace.Services.Posts/tests/MiniSpace.Services.Posts.Application.UnitTests/Commands/Handlers/ChangePostStateHandlerTest.cs @@ -0,0 +1,285 @@ +using Xunit; +using Moq; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Linq; +using System.Text; +using MiniSpace.Services.Posts.Application.Events; +using MiniSpace.Services.Posts.Application.Exceptions; +using MiniSpace.Services.Posts.Application.Services; +using MiniSpace.Services.Posts.Core.Entities; +using MiniSpace.Services.Posts.Core.Repositories; +using MiniSpace.Services.Posts.Application.Commands.Handlers; +using MiniSpace.Services.Posts.Application.Commands; +using MiniSpace.Services.Posts.Infrastructure.Contexts; +using Convey.CQRS.Commands; +using System.Threading; +using System.Security.Claims; +using FluentAssertions; +using Microsoft.OpenApi.Extensions; +using MiniSpace.Services.Posts.Core.Exceptions; +using System.Diagnostics.Eventing.Reader; + +namespace MiniSpace.Services.Posts.Application.UnitTests.Commands.Handlers { + public class ChangePostStateHandlerTest { + private readonly ChangePostStateHandler _changePostStateHandler; + private readonly Mock _postRepositoryMock; + private readonly Mock _dateTimeProviderMock; + private readonly Mock _messageBrokerMock; + private readonly Mock _appContextMock; + + public ChangePostStateHandlerTest() { + _postRepositoryMock = new(); + _messageBrokerMock = new(); + _dateTimeProviderMock = new(); + _appContextMock = new(); + _changePostStateHandler = new ChangePostStateHandler(_postRepositoryMock.Object, + _appContextMock.Object, + _dateTimeProviderMock.Object, + _messageBrokerMock.Object + ); + } + + [Fact] + public async Task HandleAsync_WithValidParametersAndStateToBePublished_ShouldPublishAsync() { + // Arrange + var contextId = Guid.NewGuid(); + var postId = Guid.NewGuid(); + var cancelationToken = new CancellationToken(); + var state = State.ToBePublished; + var eventId = Guid.NewGuid(); + + var command = new ChangePostState(postId, + state.GetDisplayName(), DateTime.Today); + + var identityContext = new IdentityContext(contextId.ToString(), "", true, default); + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + + var post = Post.Create(new AggregateId(postId), eventId, contextId, "Text", "Media content", DateTime.Today, + state, DateTime.Today); + + _postRepositoryMock.Setup(repo => repo.GetAsync(command.PostId)).ReturnsAsync(post); + + // Act + Func act = async () => await _changePostStateHandler.HandleAsync(command, cancelationToken); + await act(); + + // Assert + _messageBrokerMock.Verify(broker => broker.PublishAsync(It.IsAny()), Times.Once()); + } + + [Fact] + public async Task HandleAsync_WithValidParametersAndStateToBePublished_ShouldNotThrowException() { + // Arrange + var contextId = Guid.NewGuid(); + var postId = Guid.NewGuid(); + var cancelationToken = new CancellationToken(); + var state = State.ToBePublished; + var eventId = Guid.NewGuid(); + + var command = new ChangePostState(postId, + state.GetDisplayName(), DateTime.Today); + + var identityContext = new IdentityContext(contextId.ToString(), "", true, default); + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + + var post = Post.Create(new AggregateId(postId), eventId, contextId, "Text", "Media content", DateTime.Today, + state, DateTime.Today); + + _postRepositoryMock.Setup(repo => repo.GetAsync(command.PostId)).ReturnsAsync(post); + + // Act & Assert + Func act = async () => await _changePostStateHandler.HandleAsync(command, cancelationToken); + await act.Should().NotThrowAsync(); + } + + [Fact] + public async Task HandleAsync_WithNullPost_ShouldThrowPostNotFoundException() { + // Arrange + var contextId = Guid.NewGuid(); + var postId = Guid.NewGuid(); + var cancelationToken = new CancellationToken(); + var state = State.ToBePublished; + var eventId = Guid.NewGuid(); + + var command = new ChangePostState(postId, + state.GetDisplayName(), DateTime.Today); + + var identityContext = new IdentityContext(contextId.ToString(), "", true, default); + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + + _postRepositoryMock.Setup(repo => repo.GetAsync(command.PostId)).ReturnsAsync((Post)null); + + // Act & Assert + Func act = async () => await _changePostStateHandler.HandleAsync(command, cancelationToken); + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task HandleAsync_WithNonAuthenticated_ShouldNotThrowException() { + // Arrange + var contextId = Guid.NewGuid(); + var postId = Guid.NewGuid(); + var cancelationToken = new CancellationToken(); + var state = State.ToBePublished; + var eventId = Guid.NewGuid(); + + var command = new ChangePostState(postId, + state.GetDisplayName(), DateTime.Today); + + var identityContext = new IdentityContext(contextId.ToString(), "", false, default); + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + + var post = Post.Create(new AggregateId(postId), eventId, contextId, "Text", "Media content", DateTime.Today, + state, DateTime.Today); + + _postRepositoryMock.Setup(repo => repo.GetAsync(command.PostId)).ReturnsAsync(post); + + // Act & Assert + Func act = async () => await _changePostStateHandler.HandleAsync(command, cancelationToken); + await act.Should().NotThrowAsync(); + } + + [Fact] + public async Task HandleAsync_WithNonPermittedIdentity_ShouldThrowUnauthorizedPostAccessException() { + // Arrange + var contextId = Guid.NewGuid(); + var postId = Guid.NewGuid(); + var cancelationToken = new CancellationToken(); + var state = State.ToBePublished; + var eventId = Guid.NewGuid(); + + Guid differentOrganizer; + do { + differentOrganizer = Guid.NewGuid(); + } while (differentOrganizer == contextId); + + var command = new ChangePostState(postId, + state.GetDisplayName(), DateTime.Today); + + var post = Post.Create(new AggregateId(postId), eventId, differentOrganizer, + "Text", "Media content", DateTime.Today, + state, DateTime.Today); + + var identityContext = new IdentityContext(contextId.ToString(), "not admin", true, default); + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + + _postRepositoryMock.Setup(repo => repo.GetAsync(command.PostId)).ReturnsAsync(post); + + // Act & Assert + Func act = async () => await _changePostStateHandler.HandleAsync(command, cancelationToken); + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task HandleAsync_WithIdentityAsAdminAndForeignPost_ShouldNotThrowException() { + // Arrange + var contextId = Guid.NewGuid(); + var postId = Guid.NewGuid(); + var cancelationToken = new CancellationToken(); + var state = State.ToBePublished; + var eventId = Guid.NewGuid(); + + Guid differentOrganizer; + do { + differentOrganizer = Guid.NewGuid(); + } while (differentOrganizer == contextId); + + var command = new ChangePostState(postId, + state.GetDisplayName(), DateTime.Today); + + var post = Post.Create(new AggregateId(postId), eventId, differentOrganizer, + "Text", "Media content", DateTime.Today, + state, DateTime.Today); + + var identityContext = new IdentityContext(contextId.ToString(), "Admin", true, default); + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + + _postRepositoryMock.Setup(repo => repo.GetAsync(command.PostId)).ReturnsAsync(post); + + // Act & Assert + Func act = async () => await _changePostStateHandler.HandleAsync(command, cancelationToken); + await act.Should().NotThrowAsync(); + } + + [Fact] + public async Task HandleAsync_WithInvalidState_ShouldThrowInvalidPostStateException() { + // Arrange + var contextId = Guid.NewGuid(); + var postId = Guid.NewGuid(); + var cancelationToken = new CancellationToken(); + var state = "a"; + var eventId = Guid.NewGuid(); + + var command = new ChangePostState(postId, + state, DateTime.Today); + + var post = Post.Create(new AggregateId(postId), eventId, contextId, + "Text", "Media content", DateTime.Today, + State.ToBePublished, DateTime.Today); + + var identityContext = new IdentityContext(contextId.ToString(), "", true, default); + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + + _postRepositoryMock.Setup(repo => repo.GetAsync(command.PostId)).ReturnsAsync(post); + + // Act & Assert + Func act = async () => await _changePostStateHandler.HandleAsync(command, cancelationToken); + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task HandleAsync_WithSameState_ShouldThrowPostStateAlreadySetException() { + // Arrange + var contextId = Guid.NewGuid(); + var postId = Guid.NewGuid(); + var cancelationToken = new CancellationToken(); + var eventId = Guid.NewGuid(); + var state = State.Published; + + var command = new ChangePostState(postId, + state.GetDisplayName(), DateTime.Today); + + var post = Post.Create(new AggregateId(postId), eventId, contextId, + "Text", "Media content", DateTime.Today, + state, DateTime.Today); + + var identityContext = new IdentityContext(contextId.ToString(), "", true, default); + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + + _postRepositoryMock.Setup(repo => repo.GetAsync(command.PostId)).ReturnsAsync(post); + + // Act & Assert + Func act = async () => await _changePostStateHandler.HandleAsync(command, cancelationToken); + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task HandleAsync_WithStateToBePublishedAndNullPublishDate_ShouldThrowPublishDateNullException() { + // Arrange + var contextId = Guid.NewGuid(); + var postId = Guid.NewGuid(); + var cancelationToken = new CancellationToken(); + var eventId = Guid.NewGuid(); + var state = State.ToBePublished; + DateTime? publishDate = null; + + var command = new ChangePostState(postId, + state.GetDisplayName(), publishDate); + + var post = Post.Create(new AggregateId(postId), eventId, contextId, + "Text", "Media content", DateTime.Today, + state, DateTime.Today); + + var identityContext = new IdentityContext(contextId.ToString(), "", true, default); + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + + _postRepositoryMock.Setup(repo => repo.GetAsync(command.PostId)).ReturnsAsync(post); + + // Act & Assert + Func act = async () => await _changePostStateHandler.HandleAsync(command, cancelationToken); + await act.Should().ThrowAsync(); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Posts/tests/MiniSpace.Services.Posts.Application.UnitTests/Commands/Handlers/CreatePostHandlerTest.cs b/MiniSpace.Services.Posts/tests/MiniSpace.Services.Posts.Application.UnitTests/Commands/Handlers/CreatePostHandlerTest.cs new file mode 100644 index 00000000..64096872 --- /dev/null +++ b/MiniSpace.Services.Posts/tests/MiniSpace.Services.Posts.Application.UnitTests/Commands/Handlers/CreatePostHandlerTest.cs @@ -0,0 +1,246 @@ +using Xunit; +using Moq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using MiniSpace.Services.Posts.Application.Events; +using MiniSpace.Services.Posts.Application.Exceptions; +using MiniSpace.Services.Posts.Application.Services; +using MiniSpace.Services.Posts.Core.Entities; +using MiniSpace.Services.Posts.Core.Repositories; +using MiniSpace.Services.Posts.Application.Commands.Handlers; +using MiniSpace.Services.Posts.Application.Commands; +using MiniSpace.Services.Posts.Infrastructure.Contexts; +using Convey.CQRS.Commands; +using System.Threading; +using System.Security.Claims; +using FluentAssertions; +using MiniSpace.Services.Posts.Core.Exceptions; +using Microsoft.OpenApi.Extensions; + +namespace MiniSpace.Services.Posts.Application.UnitTests.Commands.Handlers { + public class CreatePostHandlerTest { + private readonly CreatePostHandler _createPostHandler; + private readonly Mock _postRepositoryMock; + private readonly Mock _eventRepositoryMock; + private readonly Mock _dateTimeProviderMock; + private readonly Mock _messageBrokerMock; + private readonly Mock _appContextMock; + + public CreatePostHandlerTest() { + _postRepositoryMock = new(); + _eventRepositoryMock = new(); + _dateTimeProviderMock = new(); + _messageBrokerMock = new(); + _appContextMock = new(); + _createPostHandler = new CreatePostHandler(_postRepositoryMock.Object, + _eventRepositoryMock.Object, + _dateTimeProviderMock.Object, + _messageBrokerMock.Object, + _appContextMock.Object); + } + + [Fact] + public async Task HandleAsync_WithValidParametersAndStatePublished_ShouldNotThrowException() { + // Arrange + var eventId = Guid.NewGuid(); + var contextId = Guid.NewGuid(); + var postId = Guid.NewGuid(); + var studentId = Guid.NewGuid(); + var cancelationToken = new CancellationToken(); + var state = State.Published.GetDisplayName(); + + var @event = new Event(eventId, contextId); + var command = new CreatePost(postId, eventId, contextId, "Post", "Media Content", + state, DateTime.Today); + + var identityContext = new IdentityContext(contextId.ToString(), "", true, default); + + _eventRepositoryMock.Setup(repo => repo.GetAsync(eventId)).ReturnsAsync(@event); + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + + // Act & Assert + Func act = async () => await _createPostHandler.HandleAsync(command, cancelationToken); + await act.Should().NotThrowAsync(); + } + + [Fact] + public async Task HandleAsync_WithNonAuthenticated_ShouldNotThrowException() { + // Arrange + var eventId = Guid.NewGuid(); + var contextId = Guid.NewGuid(); + var postId = Guid.NewGuid(); + var studentId = Guid.NewGuid(); + var cancelationToken = new CancellationToken(); + var state = State.Published.GetDisplayName(); + + var @event = new Event(eventId, contextId); + var command = new CreatePost(postId, eventId, contextId, "Post", "Media Content", + state, DateTime.Today); + + var isAuthenticated = false; + + var identityContext = new IdentityContext(contextId.ToString(), "", isAuthenticated, default); + + _eventRepositoryMock.Setup(repo => repo.GetAsync(eventId)).ReturnsAsync(@event); + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + + // Act & Assert + Func act = async () => await _createPostHandler.HandleAsync(command, cancelationToken); + await act.Should().NotThrowAsync(); + } + + [Fact] + public async Task HandleAsync_WithNullEvent_ShouldThrowEventNotFoundException() { + // Arrange + var eventId = Guid.NewGuid(); + var contextId = Guid.NewGuid(); + var postId = Guid.NewGuid(); + var studentId = Guid.NewGuid(); + var state = State.Published; + var cancelationToken = new CancellationToken(); + var command = new CreatePost(postId, eventId, contextId, "Post", "Media Content", + state.GetDisplayName(), DateTime.Today); + + _eventRepositoryMock.Setup(repo => repo.GetAsync(eventId)).ReturnsAsync((Event)null); + + // Act + Func act = async () => await _createPostHandler.HandleAsync(command, cancelationToken); + + // Assert + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task HandleAsync_WithNonPermittedIdentity_ShouldThrowUnauthorizedPostCreationAttemptException() { + // Arrange + var eventId = Guid.NewGuid(); + var contextId = Guid.NewGuid(); + var postId = Guid.NewGuid(); + var studentId = Guid.NewGuid(); + var cancelationToken = new CancellationToken(); + var state = State.Published; + + Guid differentOrganizer; + do { + differentOrganizer = Guid.NewGuid(); + } while (differentOrganizer == contextId); + + var @event = new Event(eventId, contextId); + var command = new CreatePost(postId, eventId, differentOrganizer, "Post", "Media Content", + state.GetDisplayName(), DateTime.Today); + + var identityContext = new IdentityContext(contextId.ToString(), "", true, default); + + _eventRepositoryMock.Setup(repo => repo.GetAsync(eventId)).ReturnsAsync(@event); + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + + // Act & Assert + Func act = async () => await _createPostHandler.HandleAsync(command, cancelationToken); + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task HandleAsync_WithIdentityNotRelatedToEvent_ShouldThrowUnauthorizedPostCreationAttemptException() { + // Arrange + var eventId = Guid.NewGuid(); + var contextId = Guid.NewGuid(); + var postId = Guid.NewGuid(); + var studentId = Guid.NewGuid(); + var cancelationToken = new CancellationToken(); + var state = State.Published; + + Guid differentOrganizer; + do { + differentOrganizer = Guid.NewGuid(); + } while (differentOrganizer == contextId); + + var @event = new Event(eventId, differentOrganizer); + var command = new CreatePost(postId, eventId, contextId, "Post", "Media Content", + state.GetDisplayName(), DateTime.Today); + + var identityContext = new IdentityContext(contextId.ToString(), "", true, default); + + _eventRepositoryMock.Setup(repo => repo.GetAsync(eventId)).ReturnsAsync(@event); + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + + // Act & Assert + Func act = async () => await _createPostHandler.HandleAsync(command, cancelationToken); + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task HandleAsync_WithInvalidStateName_ShouldThrowInvalidPostStateException() { + // Arrange + var eventId = Guid.NewGuid(); + var contextId = Guid.NewGuid(); + var postId = Guid.NewGuid(); + var studentId = Guid.NewGuid(); + var cancelationToken = new CancellationToken(); + var state = "a"; + + var @event = new Event(eventId, contextId); + var command = new CreatePost(postId, eventId, contextId, "Post", "Media Content", + state, DateTime.Today); + + var identityContext = new IdentityContext(contextId.ToString(), "", true, default); + + _eventRepositoryMock.Setup(repo => repo.GetAsync(eventId)).ReturnsAsync(@event); + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + + // Act & Assert + Func act = async () => await _createPostHandler.HandleAsync(command, cancelationToken); + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task HandleAsync_WithNewStateReported_ShouldThrowNotAllowedPostStateException() { + // Arrange + var eventId = Guid.NewGuid(); + var contextId = Guid.NewGuid(); + var postId = Guid.NewGuid(); + var studentId = Guid.NewGuid(); + var cancelationToken = new CancellationToken(); + var state = State.Reported.GetDisplayName(); + + var @event = new Event(eventId, contextId); + var command = new CreatePost(postId, eventId, contextId, "Post", "Media Content", + state, DateTime.Today); + + var identityContext = new IdentityContext(contextId.ToString(), "", true, default); + + _eventRepositoryMock.Setup(repo => repo.GetAsync(eventId)).ReturnsAsync(@event); + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + + // Act & Assert + Func act = async () => await _createPostHandler.HandleAsync(command, cancelationToken); + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task HandleAsync_WithStateToBePublishedAndNullPublishDate_ShouldThrowPublishDateNullException() { + // Arrange + var eventId = Guid.NewGuid(); + var contextId = Guid.NewGuid(); + var postId = Guid.NewGuid(); + var studentId = Guid.NewGuid(); + var cancelationToken = new CancellationToken(); + var state = State.ToBePublished.GetDisplayName(); + DateTime? publishDate = null; + + var @event = new Event(eventId, contextId); + var command = new CreatePost(postId, eventId, contextId, "Post", "Media Content", state, + publishDate); + + var identityContext = new IdentityContext(contextId.ToString(), "", true, default); + + _eventRepositoryMock.Setup(repo => repo.GetAsync(eventId)).ReturnsAsync(@event); + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + + // Act & Assert + Func act = async () => await _createPostHandler.HandleAsync(command, cancelationToken); + await act.Should().ThrowAsync(); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Posts/tests/MiniSpace.Services.Posts.Application.UnitTests/Commands/Handlers/DeletePostHandlerTest.cs b/MiniSpace.Services.Posts/tests/MiniSpace.Services.Posts.Application.UnitTests/Commands/Handlers/DeletePostHandlerTest.cs new file mode 100644 index 00000000..57d901ea --- /dev/null +++ b/MiniSpace.Services.Posts/tests/MiniSpace.Services.Posts.Application.UnitTests/Commands/Handlers/DeletePostHandlerTest.cs @@ -0,0 +1,194 @@ +using Xunit; +using Moq; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Linq; +using System.Text; +using MiniSpace.Services.Posts.Application.Events; +using MiniSpace.Services.Posts.Application.Exceptions; +using MiniSpace.Services.Posts.Application.Services; +using MiniSpace.Services.Posts.Core.Entities; +using MiniSpace.Services.Posts.Core.Repositories; +using MiniSpace.Services.Posts.Application.Commands.Handlers; +using MiniSpace.Services.Posts.Application.Commands; +using MiniSpace.Services.Posts.Infrastructure.Contexts; +using Convey.CQRS.Commands; +using System.Threading; +using System.Security.Claims; +using FluentAssertions; + +namespace MiniSpace.Services.Posts.Application.UnitTests.Commands.Handlers { + public class DeletePostHandlerTest { + private readonly DeletePostHandler _deletePostHandler; + private readonly Mock _postRepositoryMock; + private readonly Mock _messageBrokerMock; + private readonly Mock _appContextMock; + + public DeletePostHandlerTest() { + _postRepositoryMock = new(); + _messageBrokerMock = new(); + _appContextMock = new(); + _deletePostHandler = new DeletePostHandler(_postRepositoryMock.Object, + _appContextMock.Object, + _messageBrokerMock.Object + ); + } + + [Fact] + public async Task HandleAsync_WithValidParametersAndStatePublished_ShouldNotThrowException() { + // Arrange + var eventId = Guid.NewGuid(); + var contextId = Guid.NewGuid(); + var postId = Guid.NewGuid(); + var studentId = Guid.NewGuid(); + var cancelationToken = new CancellationToken(); + var state = State.Published; + + var @event = new Event(eventId, contextId); + var command = new DeletePost(postId); + + var post = Post.Create(new AggregateId(postId), eventId, contextId, + "Text", "Media content", DateTime.Today, + state, DateTime.Today); + + var identityContext = new IdentityContext(contextId.ToString(), "", true, default); + + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + _postRepositoryMock.Setup(repo => repo.GetAsync(postId)).ReturnsAsync(post); + + // Act & Assert + Func act = async () => await _deletePostHandler.HandleAsync(command, cancelationToken); + await act.Should().NotThrowAsync(); + } + + [Fact] + public async Task HandleAsync_WithNullPost_ShouldThrowPostNotFoundException() { + // Arrange + var postId = Guid.NewGuid(); + var cancelationToken = new CancellationToken(); + var command = new DeletePost(postId); + + _postRepositoryMock.Setup(repo => repo.GetAsync(postId)).ReturnsAsync((Post)null); + + // Act & Assert + Func act = async () => await _deletePostHandler.HandleAsync(command, cancelationToken); + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task HandleAsync_WithNonPermittedIdentity_ShouldThrowUnauthorizedPostAccessException() { + // Arrange + var contextId = Guid.NewGuid(); + var postId = Guid.NewGuid(); + var cancelationToken = new CancellationToken(); + var state = State.ToBePublished; + var eventId = Guid.NewGuid(); + + Guid differentOrganizer; + do { + differentOrganizer = Guid.NewGuid(); + } while (differentOrganizer == contextId); + + var command = new DeletePost(postId); + + var post = Post.Create(new AggregateId(postId), eventId, differentOrganizer, + "Text", "Media content", DateTime.Today, + state, DateTime.Today); + + var identityContext = new IdentityContext(contextId.ToString(), "not admin", true, default); + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + + _postRepositoryMock.Setup(repo => repo.GetAsync(command.PostId)).ReturnsAsync(post); + + // Act & Assert + Func act = async () => await _deletePostHandler.HandleAsync(command, cancelationToken); + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task HandleAsync_WithIdentityAsAdminAndForeignPost_ShouldNotThrowException() { + // Arrange + var contextId = Guid.NewGuid(); + var postId = Guid.NewGuid(); + var cancelationToken = new CancellationToken(); + var state = State.ToBePublished; + var eventId = Guid.NewGuid(); + + Guid differentOrganizer; + do { + differentOrganizer = Guid.NewGuid(); + } while (differentOrganizer == contextId); + + var command = new DeletePost(postId); + + var post = Post.Create(new AggregateId(postId), eventId, differentOrganizer, + "Text", "Media content", DateTime.Today, + state, DateTime.Today); + + var identityContext = new IdentityContext(contextId.ToString(), "Admin", true, default); + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + + _postRepositoryMock.Setup(repo => repo.GetAsync(command.PostId)).ReturnsAsync(post); + + // Act & Assert + Func act = async () => await _deletePostHandler.HandleAsync(command, cancelationToken); + await act.Should().NotThrowAsync(); + } + + [Fact] + public async Task HandleAsync_WithIdentityAsAdminAndReportedPost_ShouldNotThrowException() { + // Arrange + var contextId = Guid.NewGuid(); + var postId = Guid.NewGuid(); + var cancelationToken = new CancellationToken(); + var state = State.Reported; + var eventId = Guid.NewGuid(); + + Guid differentOrganizer; + do { + differentOrganizer = Guid.NewGuid(); + } while (differentOrganizer == contextId); + + var command = new DeletePost(postId); + + var post = Post.Create(new AggregateId(postId), eventId, differentOrganizer, + "Text", "Media content", DateTime.Today, + state, DateTime.Today); + + var identityContext = new IdentityContext(contextId.ToString(), "Admin", true, default); + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + + _postRepositoryMock.Setup(repo => repo.GetAsync(command.PostId)).ReturnsAsync(post); + + // Act & Assert + Func act = async () => await _deletePostHandler.HandleAsync(command, cancelationToken); + await act.Should().NotThrowAsync(); + } + + [Fact] + public async Task HandleAsync_WithIdentityAsNotAdminAndReportedPost_ShouldThrowUnauthorizedPostOperationException() { + // Arrange + var contextId = Guid.NewGuid(); + var postId = Guid.NewGuid(); + var cancelationToken = new CancellationToken(); + var state = State.Reported; + var eventId = Guid.NewGuid(); + + var command = new DeletePost(postId); + + var post = Post.Create(new AggregateId(postId), eventId, contextId, + "Text", "Media content", DateTime.Today, + state, DateTime.Today); + + var identityContext = new IdentityContext(contextId.ToString(), "not admin", true, default); + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + + _postRepositoryMock.Setup(repo => repo.GetAsync(command.PostId)).ReturnsAsync(post); + + // Act & Assert + Func act = async () => await _deletePostHandler.HandleAsync(command, cancelationToken); + await act.Should().ThrowAsync(); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Posts/tests/MiniSpace.Services.Posts.Application.UnitTests/Commands/Handlers/UpdatePostHandlerTest.cs b/MiniSpace.Services.Posts/tests/MiniSpace.Services.Posts.Application.UnitTests/Commands/Handlers/UpdatePostHandlerTest.cs new file mode 100644 index 00000000..88f7a12f --- /dev/null +++ b/MiniSpace.Services.Posts/tests/MiniSpace.Services.Posts.Application.UnitTests/Commands/Handlers/UpdatePostHandlerTest.cs @@ -0,0 +1,239 @@ +using Xunit; +using Moq; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Linq; +using System.Text; +using MiniSpace.Services.Posts.Application.Events; +using MiniSpace.Services.Posts.Application.Exceptions; +using MiniSpace.Services.Posts.Application.Services; +using MiniSpace.Services.Posts.Core.Entities; +using MiniSpace.Services.Posts.Core.Repositories; +using MiniSpace.Services.Posts.Application.Commands.Handlers; +using MiniSpace.Services.Posts.Application.Commands; +using MiniSpace.Services.Posts.Infrastructure.Contexts; +using Convey.CQRS.Commands; +using System.Threading; +using System.Security.Claims; +using FluentAssertions; + +namespace MiniSpace.Services.Posts.Application.UnitTests.Commands.Handlers { + public class UpdatePostHandlerTest { + private readonly UpdatePostHandler _updatePostHandler; + private readonly Mock _postRepositoryMock; + private readonly Mock _dateTimeProviderMock; + private readonly Mock _messageBrokerMock; + private readonly Mock _appContextMock; + + public UpdatePostHandlerTest() { + _postRepositoryMock = new(); + _dateTimeProviderMock = new(); + _messageBrokerMock = new(); + _appContextMock = new(); + _updatePostHandler = new UpdatePostHandler(_postRepositoryMock.Object, + _appContextMock.Object, + _messageBrokerMock.Object, + _dateTimeProviderMock.Object + ); + } + + [Fact] + public async Task HandleAsync_WithValidParametersAndStatePublished_ShouldNotThrowException() { + // Arrange + var eventId = Guid.NewGuid(); + var contextId = Guid.NewGuid(); + var postId = Guid.NewGuid(); + var studentId = Guid.NewGuid(); + var cancelationToken = new CancellationToken(); + var state = State.Published; + var textContent = "a"; + var mediaContent = "a"; + + var @event = new Event(eventId, contextId); + var command = new UpdatePost(postId, textContent, mediaContent); + + var post = Post.Create(new AggregateId(postId), eventId, contextId, + "Text", "Media content", DateTime.Today, + state, DateTime.Today); + + var identityContext = new IdentityContext(contextId.ToString(), "", true, default); + + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + _postRepositoryMock.Setup(repo => repo.GetAsync(postId)).ReturnsAsync(post); + + // Act & Assert + Func act = async () => await _updatePostHandler.HandleAsync(command, cancelationToken); + await act.Should().NotThrowAsync(); + } + + [Fact] + public async Task HandleAsync_WithValidParametersAndStatePublished_ShouldUpdateRepository() { + // Arrange + var eventId = Guid.NewGuid(); + var contextId = Guid.NewGuid(); + var postId = Guid.NewGuid(); + var studentId = Guid.NewGuid(); + var cancelationToken = new CancellationToken(); + var state = State.Published; + var textContent = "a"; + var mediaContent = "a"; + + var @event = new Event(eventId, contextId); + var command = new UpdatePost(postId, textContent, mediaContent); + + var post = Post.Create(new AggregateId(postId), eventId, contextId, + "Text", "Media content", DateTime.Today, + state, DateTime.Today); + + var identityContext = new IdentityContext(contextId.ToString(), "", true, default); + + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + _postRepositoryMock.Setup(repo => repo.GetAsync(postId)).ReturnsAsync(post); + + // Act & Assert + Func act = async () => await _updatePostHandler.HandleAsync(command, cancelationToken); + await act(); + _postRepositoryMock.Verify(repo => repo.UpdateAsync(post), Times.Once()); + } + + [Fact] + public async Task HandleAsync_WithNullPost_ShouldThrowPostNotFoundException() { + // Arrange + var postId = Guid.NewGuid(); + var cancelationToken = new CancellationToken(); + var textContent = "a"; + var mediaContent = "a"; + var command = new UpdatePost(postId, textContent, mediaContent); + + _postRepositoryMock.Setup(repo => repo.GetAsync(postId)).ReturnsAsync((Post)null); + + // Act & Assert + Func act = async () => await _updatePostHandler.HandleAsync(command, cancelationToken); + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task HandleAsync_WithNonPermittedIdentity_ShouldThrowUnauthorizedPostAccessException() { + // Arrange + var contextId = Guid.NewGuid(); + var postId = Guid.NewGuid(); + var cancelationToken = new CancellationToken(); + var state = State.ToBePublished; + var eventId = Guid.NewGuid(); + var textContent = "a"; + var mediaContent = "a"; + + Guid differentOrganizer; + do { + differentOrganizer = Guid.NewGuid(); + } while (differentOrganizer == contextId); + + var command = new UpdatePost(postId, textContent, mediaContent); + + var post = Post.Create(new AggregateId(postId), eventId, differentOrganizer, + "Text", "Media content", DateTime.Today, + state, DateTime.Today); + + var identityContext = new IdentityContext(contextId.ToString(), "not admin", true, default); + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + + _postRepositoryMock.Setup(repo => repo.GetAsync(command.PostId)).ReturnsAsync(post); + + // Act & Assert + Func act = async () => await _updatePostHandler.HandleAsync(command, cancelationToken); + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task HandleAsync_WithIdentityAsAdminAndForeignPost_ShouldNotThrowException() { + // Arrange + var contextId = Guid.NewGuid(); + var postId = Guid.NewGuid(); + var cancelationToken = new CancellationToken(); + var state = State.ToBePublished; + var eventId = Guid.NewGuid(); + var textContent = "a"; + var mediaContent = "a"; + + Guid differentOrganizer; + do { + differentOrganizer = Guid.NewGuid(); + } while (differentOrganizer == contextId); + + var command = new UpdatePost(postId, textContent, mediaContent); + + var post = Post.Create(new AggregateId(postId), eventId, differentOrganizer, + "Text", "Media content", DateTime.Today, + state, DateTime.Today); + + var identityContext = new IdentityContext(contextId.ToString(), "Admin", true, default); + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + + _postRepositoryMock.Setup(repo => repo.GetAsync(command.PostId)).ReturnsAsync(post); + + // Act & Assert + Func act = async () => await _updatePostHandler.HandleAsync(command, cancelationToken); + await act.Should().NotThrowAsync(); + } + + [Fact] + public async Task HandleAsync_WithIdentityAsAdminAndReportedPost_ShouldNotThrowException() { + // Arrange + var contextId = Guid.NewGuid(); + var postId = Guid.NewGuid(); + var cancelationToken = new CancellationToken(); + var state = State.Reported; + var eventId = Guid.NewGuid(); + var textContent = "a"; + var mediaContent = "a"; + + Guid differentOrganizer; + do { + differentOrganizer = Guid.NewGuid(); + } while (differentOrganizer == contextId); + + var command = new UpdatePost(postId, textContent, mediaContent); + + var post = Post.Create(new AggregateId(postId), eventId, differentOrganizer, + "Text", "Media content", DateTime.Today, + state, DateTime.Today); + + var identityContext = new IdentityContext(contextId.ToString(), "Admin", true, default); + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + + _postRepositoryMock.Setup(repo => repo.GetAsync(command.PostId)).ReturnsAsync(post); + + // Act & Assert + Func act = async () => await _updatePostHandler.HandleAsync(command, cancelationToken); + await act.Should().NotThrowAsync(); + } + + [Fact] + public async Task HandleAsync_WithIdentityAsNotAdminAndReportedPost_ShouldThrowUnauthorizedPostOperationException() { + // Arrange + var contextId = Guid.NewGuid(); + var postId = Guid.NewGuid(); + var cancelationToken = new CancellationToken(); + var state = State.Reported; + var eventId = Guid.NewGuid(); + var textContent = "a"; + var mediaContent = "a"; + + var command = new UpdatePost(postId, textContent, mediaContent); + + var post = Post.Create(new AggregateId(postId), eventId, contextId, + "Text", "Media content", DateTime.Today, + state, DateTime.Today); + + var identityContext = new IdentityContext(contextId.ToString(), "not admin", true, default); + _appContextMock.Setup(ctx => ctx.Identity).Returns(identityContext); + + _postRepositoryMock.Setup(repo => repo.GetAsync(command.PostId)).ReturnsAsync(post); + + // Act & Assert + Func act = async () => await _updatePostHandler.HandleAsync(command, cancelationToken); + await act.Should().ThrowAsync(); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Posts/tests/MiniSpace.Services.Posts.Application.UnitTests/Commands/Handlers/UpdatePostsStateHandlerTest.cs b/MiniSpace.Services.Posts/tests/MiniSpace.Services.Posts.Application.UnitTests/Commands/Handlers/UpdatePostsStateHandlerTest.cs new file mode 100644 index 00000000..891006e4 --- /dev/null +++ b/MiniSpace.Services.Posts/tests/MiniSpace.Services.Posts.Application.UnitTests/Commands/Handlers/UpdatePostsStateHandlerTest.cs @@ -0,0 +1,86 @@ +using Xunit; +using Moq; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Linq; +using System.Text; +using MiniSpace.Services.Posts.Application.Events; +using MiniSpace.Services.Posts.Application.Exceptions; +using MiniSpace.Services.Posts.Application.Services; +using MiniSpace.Services.Posts.Core.Entities; +using MiniSpace.Services.Posts.Core.Repositories; +using MiniSpace.Services.Posts.Application.Commands.Handlers; +using MiniSpace.Services.Posts.Application.Commands; +using MiniSpace.Services.Posts.Infrastructure.Contexts; +using Convey.CQRS.Commands; +using System.Threading; +using System.Security.Claims; +using FluentAssertions; + +namespace MiniSpace.Services.Posts.Application.UnitTests.Commands.Handlers { + public class UpdatePostsStateHandlerTest { + private readonly UpdatePostsStateHandler _updatePostStateHandler; + private readonly Mock _postRepositoryMock; + private readonly Mock _messageBrokerMock; + + public UpdatePostsStateHandlerTest() { + _postRepositoryMock = new(); + _messageBrokerMock = new(); + _updatePostStateHandler = new UpdatePostsStateHandler(_postRepositoryMock.Object, + _messageBrokerMock.Object + ); + } + + [Fact] + public async Task HandleAsync_WithValidParametersAndStatePublished_ShouldNotThrowException() { + // Arrange + var eventId = Guid.NewGuid(); + var contextId = Guid.NewGuid(); + var postId = Guid.NewGuid(); + var cancelationToken = new CancellationToken(); + var state = State.Published; + + var command = new UpdatePostsState(DateTime.Today); + + var post = Post.Create(new AggregateId(postId), eventId, contextId, + "Text", "Media content", DateTime.Today, + state, DateTime.Today); + + _postRepositoryMock.Setup(repo => repo.GetAsync(postId)).ReturnsAsync(post); + + // Act & Assert + Func act = async () => await _updatePostStateHandler.HandleAsync(command, cancelationToken); + await act.Should().NotThrowAsync(); + } + + [Fact] + public async Task HandleAsync_WithValidParametersAndStatePublished_ShouldUpdateRepository() { + // Arrange + var eventId = Guid.NewGuid(); + var contextId = Guid.NewGuid(); + var postId = Guid.NewGuid(); + var cancelationToken = new CancellationToken(); + var state = State.ToBePublished; + + var command = new UpdatePostsState(new DateTime(2024, 1, 1, 0, 0, 0)); + + var post = Post.Create(new AggregateId(postId), eventId, contextId, + "Text", "Media content", new DateTime(2000, 1, 1, 0, 0, 0), + state, new DateTime(2000, 1, 1, 0, 0, 0)); + + var postList = new List + { + post + }; + + _postRepositoryMock.Setup(repo => repo.GetToUpdateAsync()).ReturnsAsync(postList.AsEnumerable()); + _postRepositoryMock.Setup(repo => repo.GetAsync(postId)).ReturnsAsync(post); + + // Act & Assert + Func act = async () => await _updatePostStateHandler.HandleAsync(command, cancelationToken); + await act(); + _postRepositoryMock.Verify(repo => repo.UpdateAsync(post), Times.Once()); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Posts/tests/MiniSpace.Services.Posts.Application.UnitTests/Events/External/Handlers/EventCreatedHandlerTest.cs b/MiniSpace.Services.Posts/tests/MiniSpace.Services.Posts.Application.UnitTests/Events/External/Handlers/EventCreatedHandlerTest.cs new file mode 100644 index 00000000..4880ded0 --- /dev/null +++ b/MiniSpace.Services.Posts/tests/MiniSpace.Services.Posts.Application.UnitTests/Events/External/Handlers/EventCreatedHandlerTest.cs @@ -0,0 +1,65 @@ +using Xunit; +using Moq; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Posts.Application.Exceptions; +using MiniSpace.Services.Posts.Application.Services; +using MiniSpace.Services.Posts.Core.Entities; +using MiniSpace.Services.Posts.Core.Repositories; +using MiniSpace.Services.Posts.Application.Commands.Handlers; +using MiniSpace.Services.Posts.Application.Commands; +using MiniSpace.Services.Posts.Infrastructure.Contexts; +using System.Threading; +using FluentAssertions; +using MiniSpace.Services.Posts.Application.Events.External.Handlers; +using MiniSpace.Services.Posts.Application.Events.External; +using System.ComponentModel.Design; +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Posts.Application.UnitTests.Events.External.Handlers +{ + public class EventCreatedHandlerTest + { + private readonly EventCreatedHandler _eventDeletedHandler; + private readonly Mock _eventRepositoryMock; + + public EventCreatedHandlerTest() + { + _eventRepositoryMock = new(); + _eventDeletedHandler = new EventCreatedHandler(_eventRepositoryMock.Object); + } + + [Fact] + public async Task HandleAsync_ValidData_ShouldNotThrowExeption() + { + // Arrange + var eventId = Guid.NewGuid(); + var organizerId = Guid.NewGuid(); + var @event = new EventCreated(eventId, organizerId); + + _eventRepositoryMock.Setup(repo => repo.ExistsAsync(eventId)) + .ReturnsAsync(false); + + // Act & Assert + Func act = async () => await _eventDeletedHandler.HandleAsync(@event); + await act.Should().NotThrowAsync(); + } + + [Fact] + public async Task HandleAsync_EventAlreadyCreated_ShouldThrowEventAlreadyExistsException() + { + // Arrange + var eventId = Guid.NewGuid(); + var organizerId = Guid.NewGuid(); + var @event = new EventCreated(eventId, organizerId); + + _eventRepositoryMock.Setup(repo => repo.ExistsAsync(eventId)) + .ReturnsAsync(true); + + // Act & Assert + Func act = async () => await _eventDeletedHandler.HandleAsync(@event); + await act.Should().ThrowAsync(); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Posts/tests/MiniSpace.Services.Posts.Application.UnitTests/Events/External/Handlers/EventDeletedHandlerTest.cs b/MiniSpace.Services.Posts/tests/MiniSpace.Services.Posts.Application.UnitTests/Events/External/Handlers/EventDeletedHandlerTest.cs new file mode 100644 index 00000000..d8e372e1 --- /dev/null +++ b/MiniSpace.Services.Posts/tests/MiniSpace.Services.Posts.Application.UnitTests/Events/External/Handlers/EventDeletedHandlerTest.cs @@ -0,0 +1,55 @@ +using Xunit; +using Moq; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Posts.Application.Exceptions; +using MiniSpace.Services.Posts.Application.Services; +using MiniSpace.Services.Posts.Core.Entities; +using MiniSpace.Services.Posts.Core.Repositories; +using MiniSpace.Services.Posts.Application.Commands.Handlers; +using MiniSpace.Services.Posts.Application.Commands; +using MiniSpace.Services.Posts.Infrastructure.Contexts; +using System.Threading; +using FluentAssertions; +using MiniSpace.Services.Posts.Application.Events.External.Handlers; +using MiniSpace.Services.Posts.Application.Events.External; +using System.ComponentModel.Design; +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Posts.Application.UnitTests.Events.External.Handlers +{ + public class EventDeletedHandlerTest + { + private readonly EventDeletedHandler _eventDeletedHandler; + private readonly Mock _postRepositoryMock; + private readonly Mock _eventRepositoryMock; + private readonly Mock _commandDispatcherMock; + + public EventDeletedHandlerTest() + { + _eventRepositoryMock = new(); + _commandDispatcherMock = new(); + _postRepositoryMock = new Mock(); + _eventDeletedHandler = new EventDeletedHandler(_eventRepositoryMock.Object, + _postRepositoryMock.Object, _commandDispatcherMock.Object); + } + + [Fact] + public async Task HandleAsync_NullEvent_ShouldThrowEventNotFoundException() + { + // Arrange + var eventId = Guid.NewGuid(); + var cancelationToken = new CancellationToken(); + var @event = new EventDeleted(eventId); + + _eventRepositoryMock.Setup(repo => repo.GetAsync(eventId)).ReturnsAsync((Event)null); + + // Act + Func act = async () => await _eventDeletedHandler.HandleAsync(@event, cancelationToken); + + // Assert + await act.Should().ThrowAsync(); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Posts/tests/MiniSpace.Services.Posts.Application.UnitTests/MiniSpace.Services.Posts.Application.UnitTests.csproj b/MiniSpace.Services.Posts/tests/MiniSpace.Services.Posts.Application.UnitTests/MiniSpace.Services.Posts.Application.UnitTests.csproj new file mode 100644 index 00000000..3a793344 --- /dev/null +++ b/MiniSpace.Services.Posts/tests/MiniSpace.Services.Posts.Application.UnitTests/MiniSpace.Services.Posts.Application.UnitTests.csproj @@ -0,0 +1,33 @@ + + + + net8.0 + enable + disable + + false + true + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + diff --git a/MiniSpace.Services.Posts/tests/MiniSpace.Services.Posts.Core.UnitTests/Entities/AggregatedIdTest.cs b/MiniSpace.Services.Posts/tests/MiniSpace.Services.Posts.Core.UnitTests/Entities/AggregatedIdTest.cs new file mode 100644 index 00000000..e8f4e9c2 --- /dev/null +++ b/MiniSpace.Services.Posts/tests/MiniSpace.Services.Posts.Core.UnitTests/Entities/AggregatedIdTest.cs @@ -0,0 +1,57 @@ +using Xunit; +using Moq; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Linq; +using System.Text; +using MiniSpace.Services.Posts.Application.Events; +using MiniSpace.Services.Posts.Application.Exceptions; +using MiniSpace.Services.Posts.Application.Services; +using MiniSpace.Services.Posts.Core.Entities; +using MiniSpace.Services.Posts.Core.Repositories; +using MiniSpace.Services.Posts.Application.Commands.Handlers; +using MiniSpace.Services.Posts.Application.Commands; +using MiniSpace.Services.Posts.Infrastructure.Contexts; +using Convey.CQRS.Commands; +using System.Threading; +using System.Security.Claims; +using FluentAssertions; +using MiniSpace.Services.Posts.Core.Exceptions; +using Microsoft.AspNetCore.DataProtection.KeyManagement; + +namespace MiniSpace.Services.Posts.Core.UnitTests.Entities +{ + public class AggregatedIdTest + { + [Fact] + public void AggregateId_CreatedTwice_ShouldBeDifferent() + { + // Arrange & Act + var id1 = new AggregateId(); + var id2 = new AggregateId(); + + // Assert + Assert.NotEqual(id1.Value, id2.Value); + } + + [Fact] + public void AggregateId_CreatedTwiceSameGuid_ShouldBeSame() + { + // Arrange + var id = Guid.NewGuid(); + + // Act + var id1 = new AggregateId(id); + var id2 = new AggregateId(id); + + // Assert + Assert.Equal(id1.Value, id2.Value); + } + + [Fact] + public void AggregateId_CreateWithEmptyGuid_ShouldThrowInvalidAggregateIdException() { + Assert.Throws(() => { var id = new AggregateId(Guid.Empty); }); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Posts/tests/MiniSpace.Services.Posts.Core.UnitTests/Entities/PostTest.cs b/MiniSpace.Services.Posts/tests/MiniSpace.Services.Posts.Core.UnitTests/Entities/PostTest.cs new file mode 100644 index 00000000..988c319d --- /dev/null +++ b/MiniSpace.Services.Posts/tests/MiniSpace.Services.Posts.Core.UnitTests/Entities/PostTest.cs @@ -0,0 +1,75 @@ +using Xunit; +using Moq; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Linq; +using System.Text; +using MiniSpace.Services.Posts.Application.Events; +using MiniSpace.Services.Posts.Application.Exceptions; +using MiniSpace.Services.Posts.Application.Services; +using MiniSpace.Services.Posts.Core.Entities; +using MiniSpace.Services.Posts.Core.Repositories; +using MiniSpace.Services.Posts.Application.Commands.Handlers; +using MiniSpace.Services.Posts.Application.Commands; +using MiniSpace.Services.Posts.Infrastructure.Contexts; +using Convey.CQRS.Commands; +using System.Threading; +using System.Security.Claims; +using FluentAssertions; +using MiniSpace.Services.Posts.Core.Exceptions; +using Microsoft.AspNetCore.DataProtection.KeyManagement; + +namespace MiniSpace.Services.Posts.Core.UnitTests.Entities { + public class PostTest + { + [Fact] + public void Create_WithWhitespace_ShouldThrowInvalidPostTextContentException() { + // Arrange + var id = new AggregateId(); + string textContent = " "; + + // Act & Assert + Assert.Throws(() => { + Post.Create(id, default, default, textContent, default, default, default, default); + }); + } + + [Fact] + public void Create_WithNullTextContent_ShouldThrowInvalidPostTextContentException() { + // Arrange + var id = new AggregateId(); + string textContent = null; + + // Act & Assert + Assert.Throws(() => { + Post.Create(id, default, default, textContent, default, default, default, default); + }); + } + + [Fact] + public void Create_WithTooLongTextContent_ShouldThrowInvalidPostTextContentException() { + // Arrange + var id = new AggregateId(); + string textContent = new('a', 100000); + + // Act & Assert + Assert.Throws(() => { + Post.Create(id, default, default, textContent, default, default, default, default); + }); + } + + [Fact] + public void CheckPublishDate_WithInappropriateDateTime_ShouldThrowInvalidPostPublishDateException() { + // Arrange + var id = new AggregateId(); + string textContent = new('a', 100); + var post = Post.Create(id, default, default, textContent, default, DateTime.Now, default, DateTime.Now); + + // Act & Assert + Assert.Throws(() => { + post.SetToBePublished(new DateTime(2000, 1, 1, 1, 1, 1), DateTime.Now); + }); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Posts/tests/MiniSpace.Services.Posts.Core.UnitTests/MiniSpace.Services.Posts.Core.UnitTests.csproj b/MiniSpace.Services.Posts/tests/MiniSpace.Services.Posts.Core.UnitTests/MiniSpace.Services.Posts.Core.UnitTests.csproj new file mode 100644 index 00000000..3a793344 --- /dev/null +++ b/MiniSpace.Services.Posts/tests/MiniSpace.Services.Posts.Core.UnitTests/MiniSpace.Services.Posts.Core.UnitTests.csproj @@ -0,0 +1,33 @@ + + + + net8.0 + enable + disable + + false + true + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + diff --git a/MiniSpace.Services.Posts/tests/MiniSpace.Services.Posts.Infrastructure.UnitTests/MiniSpace.Services.Posts.Infrastructure.UnitTests.csproj b/MiniSpace.Services.Posts/tests/MiniSpace.Services.Posts.Infrastructure.UnitTests/MiniSpace.Services.Posts.Infrastructure.UnitTests.csproj new file mode 100644 index 00000000..3a793344 --- /dev/null +++ b/MiniSpace.Services.Posts/tests/MiniSpace.Services.Posts.Infrastructure.UnitTests/MiniSpace.Services.Posts.Infrastructure.UnitTests.csproj @@ -0,0 +1,33 @@ + + + + net8.0 + enable + disable + + false + true + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + diff --git a/MiniSpace.Services.Posts/tests/MiniSpace.Services.Posts.Infrastructure.UnitTests/Services/MessageBrokerTest.cs b/MiniSpace.Services.Posts/tests/MiniSpace.Services.Posts.Infrastructure.UnitTests/Services/MessageBrokerTest.cs new file mode 100644 index 00000000..6c9f6d58 --- /dev/null +++ b/MiniSpace.Services.Posts/tests/MiniSpace.Services.Posts.Infrastructure.UnitTests/Services/MessageBrokerTest.cs @@ -0,0 +1,142 @@ +using Xunit; +using Moq; +using Convey.CQRS.Events; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Convey.MessageBrokers; +using Convey.MessageBrokers.Outbox; +using Convey.MessageBrokers.RabbitMQ; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using OpenTracing; +using MiniSpace.Services.Posts.Infrastructure.Services; +using MiniSpace.Services.Posts.Application.Services; +using MiniSpace.Services.Posts.Application.Events; + +namespace MiniSpace.Services.Posts.Infrastructure.UnitTests.Services +{ + public class MessageBrokerTest + { + private readonly MessageBroker _messageBroker; + private readonly Mock _mockBusPublisher; + private readonly Mock _mockMessageOutbox; + private readonly Mock _mockContextAccessor; + private readonly Mock _mockHttpContextAccessor; + private readonly Mock _mockMessagePropertiesAccessor; + private readonly Mock _mockTracer; + private readonly Mock> _mockLogger; + + public MessageBrokerTest() + { + _mockBusPublisher = new Mock(); + _mockMessageOutbox = new Mock(); + _mockContextAccessor = new Mock(); + _mockHttpContextAccessor = new Mock(); + _mockMessagePropertiesAccessor = new Mock(); + _mockTracer = new Mock(); + _mockLogger = new Mock>(); + + _messageBroker = new MessageBroker(_mockBusPublisher.Object, _mockMessageOutbox.Object, _mockContextAccessor.Object, + _mockHttpContextAccessor.Object, _mockMessagePropertiesAccessor.Object, new RabbitMqOptions(), + _mockTracer.Object, _mockLogger.Object); + } + + [Fact] + public async Task PublishAsync_WithEventsAndOutboxDisabled_PublishesEvents() + { + //Arrange + var events = new List + { + new PostCreated(Guid.NewGuid()) + }; + _mockMessageOutbox.Setup(x => x.Enabled).Returns(false); + + //Act + await _messageBroker.PublishAsync(events); + + //Assert + _mockMessageOutbox.Verify( + x => x.SendAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny>()), + Times.Never + ); + _mockBusPublisher.Verify( + x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny>()), Times.Exactly(events.Count) + ); + } + + [Fact] + public async Task PublishAsync_WithEventsAndOutboxEnabled_SendsMessagesToOutbox() + { + //Arrange + var events = new List + { + new PostCreated(Guid.NewGuid()) + }; + _mockMessageOutbox.Setup(x => x.Enabled).Returns(true); + + //Act + await _messageBroker.PublishAsync(events); + + //Assert + _mockMessageOutbox.Verify( + x => x.SendAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny>()), + Times.Exactly(events.Count) + ); + _mockBusPublisher.Verify( + x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny>()), Times.Never + ); + } + + [Fact] + public async Task PublishAsync_WithoutEvents_Returns() + { + //Arrange + List events = null; + + //Act + await _messageBroker.PublishAsync(events); + + //Assert + _mockMessageOutbox.Verify( + x => x.SendAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny>()), + Times.Never + ); + _mockBusPublisher.Verify( + x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny>()), Times.Never + ); + } + + [Fact] + public async Task PublishAsync_WithNullEventAndOutboxDisabled_PublishesOneLessEvent() + { + //Arrange + var events = new List + { + new PostCreated(Guid.NewGuid()), + null + }; + _mockMessageOutbox.Setup(x => x.Enabled).Returns(false); + + //Act + await _messageBroker.PublishAsync(events); + + //Assert + _mockMessageOutbox.Verify( + x => x.SendAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny>()), + Times.Never + ); + _mockBusPublisher.Verify( + x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny>()), Times.Exactly(events.Count - 1) + ); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Posts/tests/MiniSpace.Services.Posts.Infrastructure.UnitTests/Services/Workers/PostStateUpdaterWorkerTest.cs b/MiniSpace.Services.Posts/tests/MiniSpace.Services.Posts.Infrastructure.UnitTests/Services/Workers/PostStateUpdaterWorkerTest.cs new file mode 100644 index 00000000..2258332b --- /dev/null +++ b/MiniSpace.Services.Posts/tests/MiniSpace.Services.Posts.Infrastructure.UnitTests/Services/Workers/PostStateUpdaterWorkerTest.cs @@ -0,0 +1,95 @@ +using Xunit; +using Moq; +using Convey.CQRS.Events; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Convey.MessageBrokers; +using Convey.MessageBrokers.Outbox; +using Convey.MessageBrokers.RabbitMQ; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using OpenTracing; +using MiniSpace.Services.Posts.Infrastructure.Services; +using MiniSpace.Services.Posts.Application.Services; +using MiniSpace.Services.Posts.Application.Events; +using MiniSpace.Services.Posts.Infrastructure.Services.Workers; +using Convey.CQRS.Commands; +using Microsoft.VisualStudio.TestPlatform.Common.Utilities; +using App.Metrics.Timer; +using MiniSpace.Services.Posts.Application.Commands; + +namespace MiniSpace.Services.Posts.Infrastructure.UnitTests.Services.Workers +{ + public class PostStateUpdaterWorkerTest + { + private readonly PostStateUpdaterWorker _postStateUpdaterWorker; + private readonly Mock _messageBrokerMock; + private readonly Mock _commandDispatcherMock; + private readonly Mock _dateTimeProviderMock; + + public PostStateUpdaterWorkerTest() + { + _messageBrokerMock = new Mock(); + _commandDispatcherMock = new Mock(); + _dateTimeProviderMock = new Mock(); + _postStateUpdaterWorker = new PostStateUpdaterWorker(_messageBrokerMock.Object, + _commandDispatcherMock.Object, _dateTimeProviderMock.Object); + } + + [Fact] + public async Task ExecuteAsync_WithDefaultParameters_ShouldPublishStarted() + { + // Arrange + CancellationToken cancellationToken = new CancellationToken(); + + // Act + await _postStateUpdaterWorker.StartAsync(cancellationToken); + await Task.Delay(1000); + + // Assert + _messageBrokerMock.Verify(broker => + broker.PublishAsync(It.IsAny()), + Times.Once()); + } + + [Fact] + public async Task ExecuteAsync_WithCancelRequested_ShouldPublishStopped() + { + // Arrange + CancellationToken cancellationToken = new CancellationToken(); + + // Act + await _postStateUpdaterWorker.StartAsync(cancellationToken); + await Task.Delay(1000); + await _postStateUpdaterWorker.StopAsync(cancellationToken); + + // Assert + _messageBrokerMock.Verify(broker => + broker.PublishAsync(It.IsAny()), + Times.Once()); + } + + [Fact] + public async Task ExecuteAsync_WithTimeSetToInvokeCommandDispatcherSendAsync_ShouldCommandDispatcherSendAsync() + { + CancellationToken cancellationToken = new CancellationToken(); + var nowMock0 = new DateTime(2024, 5, 26, 18, PostStateUpdaterWorker.MinutesInterval * 1, 0); + var nowMock1 = new DateTime(2024, 5, 26, 18, PostStateUpdaterWorker.MinutesInterval * 2, 0); + + _dateTimeProviderMock.Setup(prov => prov.Now).Returns(nowMock0); + await _postStateUpdaterWorker.StartAsync(cancellationToken); + await Task.Delay(1000); + await _postStateUpdaterWorker.StopAsync(cancellationToken); + + _dateTimeProviderMock.Setup(prov => prov.Now).Returns(nowMock1); + await _postStateUpdaterWorker.StartAsync(cancellationToken); + await Task.Delay(1000); + await _postStateUpdaterWorker.StopAsync(cancellationToken); + + _commandDispatcherMock.Verify(broker => + broker.SendAsync(It.IsAny(), It.IsAny()), + Times.Exactly(2)); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/CompleteStudentRegistration.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/CompleteStudentRegistration.cs index 11713de3..7c159bb6 100644 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/CompleteStudentRegistration.cs +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/CompleteStudentRegistration.cs @@ -5,12 +5,12 @@ namespace MiniSpace.Services.Students.Application.Commands public class CompleteStudentRegistration : ICommand { public Guid StudentId { get; } - public string ProfileImage { get; } + public Guid ProfileImage { get; } public string Description { get; } public DateTime DateOfBirth { get; } public bool EmailNotifications { get; } - public CompleteStudentRegistration(Guid studentId, string profileImage, + public CompleteStudentRegistration(Guid studentId, Guid profileImage, string description, DateTime dateOfBirth, bool emailNotifications) { StudentId = studentId; diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/UpdateStudent.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/UpdateStudent.cs index d76bb3ba..ecd0e256 100644 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/UpdateStudent.cs +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/UpdateStudent.cs @@ -5,11 +5,11 @@ namespace MiniSpace.Services.Students.Application.Commands public class UpdateStudent : ICommand { public Guid StudentId { get; } - public string ProfileImage { get; } + public Guid ProfileImage { get; } public string Description { get; } public bool EmailNotifications { get; } - public UpdateStudent(Guid studentId, string profileImage, string description, bool emailNotifications) + public UpdateStudent(Guid studentId, Guid profileImage, string description, bool emailNotifications) { StudentId = studentId; ProfileImage = profileImage; diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/StudentDto.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/StudentDto.cs index 6f8d2768..25dc39f0 100644 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/StudentDto.cs +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/StudentDto.cs @@ -7,7 +7,7 @@ public class StudentDto public string FirstName { get; set; } public string LastName { get; set; } public int NumberOfFriends { get; set; } - public string ProfileImage { get; set; } + public Guid ProfileImage { get; set; } public string Description { get; set; } public DateTime? DateOfBirth { get; set; } public bool EmailNotifications { get; set; } diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/MediaFileDeletedHandler.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/MediaFileDeletedHandler.cs new file mode 100644 index 00000000..bb3dfc3f --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/MediaFileDeletedHandler.cs @@ -0,0 +1,30 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Students.Core.Repositories; + +namespace MiniSpace.Services.Students.Application.Events.External.Handlers +{ + public class MediaFileDeletedHandler: IEventHandler + { + private readonly IStudentRepository _studentRepository; + + public MediaFileDeletedHandler(IStudentRepository studentRepository) + { + _studentRepository = studentRepository; + } + + public async Task HandleAsync(MediaFileDeleted @event, CancellationToken cancellationToken) + { + if(@event.Source.ToLowerInvariant() != "studentprofile") + { + return; + } + + var student = await _studentRepository.GetAsync(@event.SourceId); + if(student != null) + { + student.RemoveProfileImage(@event.MediaFileId); + await _studentRepository.UpdateAsync(student); + } + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/StudentCancelledInterestInEventHandler.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/StudentCancelledInterestInEventHandler.cs new file mode 100644 index 00000000..856455f3 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/StudentCancelledInterestInEventHandler.cs @@ -0,0 +1,37 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Students.Application.Exceptions; +using MiniSpace.Services.Students.Application.Services; +using MiniSpace.Services.Students.Core.Repositories; + +namespace MiniSpace.Services.Students.Application.Events.External.Handlers +{ + public class StudentCancelledInterestInEventHandler : IEventHandler + { + private readonly IStudentRepository _studentRepository; + private readonly IEventMapper _eventMapper; + private readonly IMessageBroker _messageBroker; + + public StudentCancelledInterestInEventHandler(IStudentRepository studentRepository, + IEventMapper eventMapper, IMessageBroker messageBroker) + { + _studentRepository = studentRepository; + _eventMapper = eventMapper; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(StudentCancelledInterestInEvent @event, CancellationToken cancellationToken) + { + var student = await _studentRepository.GetAsync(@event.StudentId); + if (student is null) + { + throw new StudentNotFoundException(@event.StudentId); + } + + student.RemoveInterestedInEvent(@event.EventId); + await _studentRepository.UpdateAsync(student); + + var events = _eventMapper.MapAll(student.Events); + await _messageBroker.PublishAsync(events.ToArray()); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/StudentCancelledSignUpToEventHandler.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/StudentCancelledSignUpToEventHandler.cs new file mode 100644 index 00000000..af207434 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/StudentCancelledSignUpToEventHandler.cs @@ -0,0 +1,37 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Students.Application.Exceptions; +using MiniSpace.Services.Students.Application.Services; +using MiniSpace.Services.Students.Core.Repositories; + +namespace MiniSpace.Services.Students.Application.Events.External.Handlers +{ + public class StudentCancelledSignUpToEventHandler : IEventHandler + { + private readonly IStudentRepository _studentRepository; + private readonly IEventMapper _eventMapper; + private readonly IMessageBroker _messageBroker; + + public StudentCancelledSignUpToEventHandler(IStudentRepository studentRepository, + IEventMapper eventMapper, IMessageBroker messageBroker) + { + _studentRepository = studentRepository; + _eventMapper = eventMapper; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(StudentCancelledSignUpToEvent @event, CancellationToken cancellationToken) + { + var student = await _studentRepository.GetAsync(@event.StudentId); + if (student is null) + { + throw new StudentNotFoundException(@event.StudentId); + } + + student.RemoveSignedUpEvent(@event.EventId); + await _studentRepository.UpdateAsync(student); + + var events = _eventMapper.MapAll(student.Events); + await _messageBroker.PublishAsync(events.ToArray()); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/MediaFileDeleted.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/MediaFileDeleted.cs new file mode 100644 index 00000000..98e856a6 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/MediaFileDeleted.cs @@ -0,0 +1,20 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.Students.Application.Events.External +{ + [Message("mediafiles")] + public class MediaFileDeleted: IEvent + { + public Guid MediaFileId { get; } + public Guid SourceId { get; } + public string Source { get; } + + public MediaFileDeleted(Guid mediaFileId, Guid sourceId, string source) + { + MediaFileId = mediaFileId; + SourceId = sourceId; + Source = source; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/StudentCancelledInterestInEvent.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/StudentCancelledInterestInEvent.cs new file mode 100644 index 00000000..a98902d3 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/StudentCancelledInterestInEvent.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.Students.Application.Events.External +{ + [Message("events")] + public class StudentCancelledInterestInEvent : IEvent + { + public Guid EventId { get; } + public Guid StudentId { get; } + + public StudentCancelledInterestInEvent(Guid eventId, Guid studentId) + { + EventId = eventId; + StudentId = studentId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/StudentCancelledSignUpToEvent.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/StudentCancelledSignUpToEvent.cs new file mode 100644 index 00000000..a473e59a --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/StudentCancelledSignUpToEvent.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.Students.Application.Events.External +{ + [Message("events")] + public class StudentCancelledSignUpToEvent : IEvent + { + public Guid EventId { get; } + public Guid StudentId { get; } + + public StudentCancelledSignUpToEvent(Guid eventId, Guid studentId) + { + EventId = eventId; + StudentId = studentId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/StudentCreated.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/StudentCreated.cs index ec71658d..6028518c 100644 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/StudentCreated.cs +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/StudentCreated.cs @@ -6,11 +6,13 @@ public class StudentCreated : IEvent { public Guid StudentId { get; } public string FullName { get; } + public Guid MediaFileId { get; } - public StudentCreated(Guid studentId, string fullName) + public StudentCreated(Guid studentId, string fullName, Guid mediaFileId) { StudentId = studentId; FullName = fullName; + MediaFileId = mediaFileId; } } } diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/StudentUpdated.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/StudentUpdated.cs index fa432404..3968ccd0 100644 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/StudentUpdated.cs +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/StudentUpdated.cs @@ -6,11 +6,13 @@ public class StudentUpdated : IEvent { public Guid StudentId { get; } public string FullName { get; } + public Guid MediaFileId { get; } - public StudentUpdated(Guid studentId, string fullName) + public StudentUpdated(Guid studentId, string fullName, Guid mediaFileId) { StudentId = studentId; FullName = fullName; + MediaFileId = mediaFileId; } } } diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/Student.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/Student.cs index e627dd49..d183d7ad 100644 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/Student.cs +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/Student.cs @@ -13,7 +13,7 @@ public class Student : AggregateRoot public string LastName { get; private set; } public string FullName => $"{FirstName} {LastName}"; public int NumberOfFriends { get; private set; } - public string ProfileImage { get; private set; } + public Guid ProfileImage { get; private set; } public string Description { get; private set; } public DateTime? DateOfBirth { get; private set; } public bool EmailNotifications { get; private set; } @@ -34,14 +34,14 @@ public IEnumerable SignedUpEvents } public Student(Guid id, string firstName, string lastName, string email, DateTime createdAt) - : this(id, email, createdAt, firstName, lastName, 0, string.Empty, string.Empty, null, + : this(id, email, createdAt, firstName, lastName, 0, Guid.Empty, string.Empty, null, false, false, false, State.Incomplete, Enumerable.Empty(), Enumerable.Empty()) { CheckFullName(firstName, lastName); } public Student(Guid id, string email, DateTime createdAt, string firstName, string lastName, - int numberOfFriends, string profileImage, string description, DateTime? dateOfBirth, + int numberOfFriends, Guid profileImage, string description, DateTime? dateOfBirth, bool emailNotifications, bool isBanned, bool isOrganizer, State state, IEnumerable interestedInEvents = null, IEnumerable signedUpEvents = null) { @@ -73,10 +73,9 @@ private void SetState(State state) AddEvent(new StudentStateChanged(this, previousState)); } - public void CompleteRegistration(string profileImage, string description, + public void CompleteRegistration(Guid profileImage, string description, DateTime dateOfBirth, DateTime now, bool emailNotifications) { - CheckProfileImage(profileImage); CheckDescription(description); CheckDateOfBirth(dateOfBirth, now); @@ -94,9 +93,8 @@ private void SetState(State state) AddEvent(new StudentRegistrationCompleted(this)); } - public void Update(string profileImage, string description, bool emailNotifications) + public void Update(Guid profileImage, string description, bool emailNotifications) { - CheckProfileImage(profileImage); CheckDescription(description); if (State != State.Valid) @@ -119,14 +117,6 @@ private void CheckFullName(string firstName, string lastName) } } - private void CheckProfileImage(string profileImage) - { - if (string.IsNullOrWhiteSpace(profileImage)) - { - throw new InvalidStudentProfileImageException(Id, profileImage); - } - } - private void CheckDescription(string description) { if (string.IsNullOrWhiteSpace(description)) @@ -152,6 +142,9 @@ public void AddInterestedInEvent(Guid eventId) _interestedInEvents.Add(eventId); } + + public void RemoveInterestedInEvent(Guid eventId) + => _interestedInEvents.Remove(eventId); public void AddSignedUpEvent(Guid eventId) { @@ -162,6 +155,19 @@ public void AddSignedUpEvent(Guid eventId) _signedUpEvents.Add(eventId); } + + public void RemoveSignedUpEvent(Guid eventId) + => _signedUpEvents.Remove(eventId); + + public void RemoveProfileImage(Guid mediaFileId) + { + if (ProfileImage != mediaFileId) + { + return; + } + + ProfileImage = Guid.Empty; + } public void Ban() => IsBanned = true; public void Unban() => IsBanned = false; diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Extensions.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Extensions.cs index 7232e3fd..062c8c8c 100644 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Extensions.cs +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Extensions.cs @@ -92,7 +92,9 @@ public static IApplicationBuilder UseInfrastructure(this IApplicationBuilder app .SubscribeCommand() .SubscribeEvent() .SubscribeEvent() + .SubscribeEvent() .SubscribeEvent() + .SubscribeEvent() .SubscribeEvent() .SubscribeEvent() .SubscribeEvent() diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Logging/MessageToLogTemplateMapper.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Logging/MessageToLogTemplateMapper.cs index d77c3aea..a3d589b2 100644 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Logging/MessageToLogTemplateMapper.cs +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Logging/MessageToLogTemplateMapper.cs @@ -45,12 +45,24 @@ internal sealed class MessageToLogTemplateMapper : IMessageToLogTemplateMapper After = "A student with id: {StudentId} has been interested in the event with id: {EventId}." } }, + { + typeof(StudentCancelledInterestInEvent), new HandlerLogTemplate + { + After = "A student with id: {StudentId} has cancelled interest in the event with id: {EventId}." + } + }, { typeof(StudentSignedUpToEvent), new HandlerLogTemplate { After = "A student with id: {StudentId} has signed up for the event with id: {EventId}." } }, + { + typeof(StudentCancelledSignUpToEvent), new HandlerLogTemplate + { + After = "A student with id: {StudentId} has cancelled sign up for the event with id: {EventId}." + } + }, { typeof(UserBanned), new HandlerLogTemplate { diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/StudentDocument.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/StudentDocument.cs index 194ef003..694a8fd1 100644 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/StudentDocument.cs +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/StudentDocument.cs @@ -10,7 +10,7 @@ public class StudentDocument : IIdentifiable public string FirstName { get; set; } public string LastName { get; set; } public int NumberOfFriends { get; set; } - public string ProfileImage { get; set; } + public Guid ProfileImage { get; set; } public string Description { get; set; } public DateTime? DateOfBirth { get; set; } public bool EmailNotifications { get; set; } diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Services/EventMapper.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Services/EventMapper.cs index 890c6021..f540eca2 100644 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Services/EventMapper.cs +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Services/EventMapper.cs @@ -15,9 +15,9 @@ public IEvent Map(IDomainEvent @event) switch (@event) { case StudentRegistrationCompleted e: - return new Application.Events.StudentCreated(e.Student.Id, e.Student.FullName); + return new Application.Events.StudentCreated(e.Student.Id, e.Student.FullName, e.Student.ProfileImage); case StudentUpdated e: - return new Application.Events.StudentUpdated(e.Student.Id, e.Student.FullName); + return new Application.Events.StudentUpdated(e.Student.Id, e.Student.FullName, e.Student.ProfileImage); case StudentStateChanged e: return new Application.Events.StudentStateChanged(e.Student.Id, e.Student.FullName, e.Student.State.ToString().ToLowerInvariant(), e.PreviousState.ToString().ToLowerInvariant()); diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Events/EventsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Events/EventsService.cs index 86d766af..ea252e42 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Events/EventsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Events/EventsService.cs @@ -25,32 +25,39 @@ public Task GetEventAsync(Guid eventId) return _httpClient.GetAsync($"events/{eventId}"); } - public Task>> GetStudentEventsAsync(Guid studentId, int numberOfResults) + public Task>> GetStudentEventsAsync(Guid studentId, + string engagementType, int page, int numberOfResults) { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); return _httpClient.GetAsync>>( - $"events/student/{studentId}?numberOfResults={numberOfResults}"); + $"events/student/{studentId}?engagementType={engagementType}&page={page}&numberOfResults={numberOfResults}"); } - public Task> AddEventAsync(Guid eventId, string name, Guid organizerId, Guid organizationId, - string startDate, string endDate, string buildingName, string street, string buildingNumber, - string apartmentNumber, string city, string zipCode, string description, int capacity, decimal fee, - string category, string publishDate) + public Task> CreateEventAsync(Guid eventId, string name, Guid organizerId, Guid organizationId, + Guid rootOrganizationId, string startDate, string endDate, string buildingName, string street, + string buildingNumber, string apartmentNumber, string city, string zipCode, IEnumerable mediaFiles, + string description, int capacity, decimal fee, string category, string publishDate) { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); return _httpClient.PostAsync("events", new {eventId, name, organizerId, organizationId, - startDate, endDate, buildingName, street, buildingNumber, apartmentNumber, city, zipCode, description, - capacity, fee, category, publishDate}); + rootOrganizationId, startDate, endDate, buildingName, street, buildingNumber, apartmentNumber, city, + zipCode, mediaFiles, description, capacity, fee, category, publishDate}); } public Task> UpdateEventAsync(Guid eventId, string name, Guid organizerId, string startDate, string endDate, string buildingName, string street, string buildingNumber, string apartmentNumber, string city, string zipCode, - string description, int capacity, decimal fee, string category, string publishDate) + IEnumerable mediaFiles, string description, int capacity, decimal fee, string category, string publishDate) { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); return _httpClient.PutAsync($"events/{eventId}", new {eventId, name, organizerId, - startDate, endDate, buildingName, street, buildingNumber, apartmentNumber, city, zipCode, description, - capacity, fee, category, publishDate}); + startDate, endDate, buildingName, street, buildingNumber, apartmentNumber, city, zipCode, mediaFiles, + description, capacity, fee, category, publishDate}); + } + + public Task DeleteEventAsync(Guid eventId) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return _httpClient.DeleteAsync($"events/{eventId}"); } public Task SignUpToEventAsync(Guid eventId, Guid studentId) @@ -83,12 +90,13 @@ public Task RateEventAsync(Guid eventId, int rating, Guid studentId) return _httpClient.PostAsync($"events/{eventId}/rate", new {eventId, rating, studentId}); } - public Task>>> SearchEventsAsync(string name, - string organizer, string category, string state, IEnumerable friends, string friendsEngagementType, - string dateFrom, string dateTo, PageableDto pageable) + public Task>>> SearchEventsAsync(string name, string organizer, + Guid organizationId, Guid rootOrganizationId, string category, string state, IEnumerable friends, + string friendsEngagementType, string dateFrom, string dateTo, PageableDto pageable) { return _httpClient.PostAsync>>("events/search", - new (name, organizer, category, state, friends, friendsEngagementType, dateFrom, dateTo, pageable)); + new (name, organizer, organizationId, rootOrganizationId, category, state, friends, + friendsEngagementType, dateFrom, dateTo, pageable)); } public Task>>> SearchOrganizerEventsAsync(Guid organizerId, @@ -98,5 +106,23 @@ public Task RateEventAsync(Guid eventId, int rating, Guid studentId) return _httpClient.PostAsync>>("events/search/organizer", new (name, organizerId, dateFrom, dateTo, state, pageable)); } + + public Task GetEventParticipants(Guid eventId) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return _httpClient.GetAsync($"events/{eventId}/participants"); + } + + public Task AddEventParticipant(Guid eventId, Guid studentId, string studentName) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return _httpClient.PostAsync($"events/{eventId}/participants", new {eventId, studentId, studentName}); + } + + public Task RemoveEventParticipant(Guid eventId, Guid participantId) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return _httpClient.DeleteAsync($"events/{eventId}/participants?participantId={participantId}"); + } } } \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Events/IEventsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Events/IEventsService.cs index 25cfc42f..7b55fcc1 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Events/IEventsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Events/IEventsService.cs @@ -11,24 +11,29 @@ namespace MiniSpace.Web.Areas.Events public interface IEventsService { Task GetEventAsync(Guid eventId); - Task>> GetStudentEventsAsync(Guid studentId, int numberOfResults); - Task> AddEventAsync(Guid eventId, string name, Guid organizerId, Guid organizationId, - string startDate, string endDate, string buildingName, string street, string buildingNumber, - string apartmentNumber, string city, string zipCode, string description, int capacity, decimal fee, - string category, string publishDate); + Task>> GetStudentEventsAsync(Guid studentId, + string engagementType, int page, int numberOfResults); + Task> CreateEventAsync(Guid eventId, string name, Guid organizerId, Guid organizationId, + Guid rootOrganizationId, string startDate, string endDate, string buildingName, string street, + string buildingNumber, string apartmentNumber, string city, string zipCode, IEnumerable mediaFiles, + string description, int capacity, decimal fee, string category, string publishDate); Task> UpdateEventAsync(Guid eventId, string name, Guid organizerId, string startDate, string endDate, string buildingName, string street, string buildingNumber, - string apartmentNumber, string city, string zipCode, string description, int capacity, decimal fee, - string category, string publishDate); + string apartmentNumber, string city, string zipCode, IEnumerable mediaFiles, string description, + int capacity, decimal fee, string category, string publishDate); + Task DeleteEventAsync(Guid eventId); Task SignUpToEventAsync(Guid eventId, Guid studentId); Task CancelSignUpToEventAsync(Guid eventId, Guid studentId); Task ShowInterestInEventAsync(Guid eventId, Guid studentId); Task CancelInterestInEventAsync(Guid eventId, Guid studentId); Task RateEventAsync(Guid eventId, int rating, Guid studentId); Task>>> SearchEventsAsync(string name, string organizer, - string category, string state, IEnumerable friends, string friendsEngagementType, string dateFrom, - string dateTo, PageableDto pageable); + Guid organizationId, Guid rootOrganizationId, string category, string state, IEnumerable friends, + string friendsEngagementType, string dateFrom, string dateTo, PageableDto pageable); Task>>> SearchOrganizerEventsAsync(Guid organizerId, string name, string state, string dateFrom, string dateTo, PageableDto pageable); + Task GetEventParticipants(Guid eventId); + Task AddEventParticipant(Guid eventId, Guid studentId, string studentName); + Task RemoveEventParticipant(Guid eventId, Guid participantId); } } \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Friends/FriendsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Friends/FriendsService.cs index ca3e71a0..bef80166 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Friends/FriendsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Friends/FriendsService.cs @@ -13,6 +13,7 @@ public class FriendsService : IFriendsService { private readonly IHttpClient _httpClient; private readonly IIdentityService _identityService; + public FriendDto FriendDto { get; private set; } @@ -43,25 +44,17 @@ public async Task> GetAllFriendsAsync(Guid studentId) { string accessToken = await _identityService.GetAccessTokenAsync(); _httpClient.SetAccessToken(accessToken); - string url = $"friends/{studentId}"; - var friends = await _httpClient.GetAsync>(url); - - Console.WriteLine($"Retrieved {friends.Count()} friends for student ID {studentId}."); + var url = $"friends/{studentId}"; + var studentFriends = await _httpClient.GetAsync>(url); - if (friends != null && friends.Any()) - { - foreach (var friend in friends) - { - friend.StudentDetails = await GetStudentAsync(friend.FriendId); - Console.WriteLine($"Friend ID: {friend.FriendId}, Friend's Student ID: {friend.StudentDetails.Id}, Name: {friend.StudentDetails.FirstName} {friend.StudentDetails.LastName}"); - } - } - else + var allFriends = studentFriends.SelectMany(sf => sf.Friends).ToList(); + + foreach (var friend in allFriends) { - Console.WriteLine("No friends found."); + friend.StudentDetails = await GetStudentAsync(friend.FriendId); } - return friends; + return allFriends; } public async Task> AddFriendAsync(Guid friendId) @@ -76,22 +69,19 @@ public async Task RemoveFriendAsync(Guid friendId) string accessToken = await _identityService.GetAccessTokenAsync(); _httpClient.SetAccessToken(accessToken); var requesterId = _identityService.GetCurrentUserId(); - Console.WriteLine($"Requester ID: {requesterId}"); // Log the requester ID + // Console.WriteLine($"Requester ID: {requesterId}"); // Log the requester ID if (requesterId == Guid.Empty) { - Console.WriteLine("Invalid Requester ID: ID is empty."); + // Console.WriteLine("Invalid Requester ID: ID is empty."); return; // Optionally handle the case where the requester ID is invalid } var payload = new { RequesterId = requesterId, FriendId = friendId }; - Console.WriteLine($"Payload: {payload.RequesterId}, {payload.FriendId}"); + // Console.WriteLine($"Payload: {payload.RequesterId}, {payload.FriendId}"); await _httpClient.DeleteAsync($"friends/{requesterId}/{friendId}/remove"); } - - - public async Task> GetAllStudentsAsync() { if (_httpClient == null) throw new InvalidOperationException("HTTP client is not initialized."); @@ -160,38 +150,94 @@ public async Task> GetSentFriendRequestsAsync() } _httpClient.SetAccessToken(accessToken); - var friendRequests = await _httpClient.GetAsync>($"friends/requests/sent/{studentId}"); + var studentRequests = await _httpClient.GetAsync>($"friends/requests/sent/{studentId}"); + + if (studentRequests == null || !studentRequests.Any()) + { + return Enumerable.Empty(); + } + + var friendRequests = studentRequests.SelectMany(request => request.FriendRequests).ToList(); + + var inviteeIds = friendRequests.Select(r => r.InviteeId).Distinct(); + var userDetailsTasks = inviteeIds.Select(id => GetUserDetails(id)); + var userDetailsResults = await Task.WhenAll(userDetailsTasks); + + var userDetailsDict = userDetailsResults.ToDictionary(user => user.Id, user => user); foreach (var request in friendRequests) { - var userDetails = await GetUserDetails(request.InviteeId); - request.InviteeName = userDetails.FirstName + " " + userDetails.LastName; - request.InviteeEmail = userDetails.Email; - request.InviteeImage = userDetails.ProfileImage; + if (userDetailsDict.TryGetValue(request.InviteeId, out var userDetails)) + { + request.InviteeName = $"{userDetails.FirstName} {userDetails.LastName}"; + request.InviteeEmail = userDetails.Email; + // request.InviteeImage = userDetails.ProfileImage; // Uncomment if you have a profile image field + } } return friendRequests; } catch (Exception ex) { + // Log the exception (optional) + // Console.WriteLine($"Error retrieving sent friend requests: {ex.Message}"); return new List(); } } public async Task> GetIncomingFriendRequestsAsync() { - var userId = _identityService.GetCurrentUserId(); - if (userId == Guid.Empty) + try { - throw new InvalidOperationException("User ID is not valid."); - } + var userId = _identityService.GetCurrentUserId(); + if (userId == Guid.Empty) + { + throw new InvalidOperationException("User ID is not valid."); + } - string accessToken = await _identityService.GetAccessTokenAsync(); - _httpClient.SetAccessToken(accessToken); - var endpoint = $"friends/requests/{userId}"; - return await _httpClient.GetAsync>(endpoint); + string accessToken = await _identityService.GetAccessTokenAsync(); + _httpClient.SetAccessToken(accessToken); + + var studentRequests = await _httpClient.GetAsync>($"friends/requests/{userId}"); + if (studentRequests == null || !studentRequests.Any()) + { + return Enumerable.Empty(); + } + + var incomingRequests = studentRequests.SelectMany(request => request.FriendRequests).ToList(); + + var inviterIds = incomingRequests.Select(r => r.InviterId).Distinct(); + var userDetailsTasks = inviterIds.Select(id => GetUserDetails(id)); + var userDetailsResults = await Task.WhenAll(userDetailsTasks); + + var userDetailsDict = userDetailsResults.ToDictionary(user => user.Id, user => user); + + foreach (var request in incomingRequests) + { + if (userDetailsDict.TryGetValue(request.InviterId, out var userDetails)) + { + request.InviterName = $"{userDetails.FirstName} {userDetails.LastName}"; + request.InviterEmail = userDetails.Email; + } + } + + return incomingRequests; + } + catch (Exception ex) + { + // Console.WriteLine($"Error retrieving incoming friend requests: {ex.Message}"); + return new List(); + } } + + // private async Task GetUserDetails(Guid userId) + // { + // string accessToken = await _identityService.GetAccessTokenAsync(); + // _httpClient.SetAccessToken(accessToken); + // return await _httpClient.GetAsync($"students/{userId}"); + // } + public async Task AcceptFriendRequestAsync(Guid requestId, Guid requesterId, Guid friendId) { string accessToken = await _identityService.GetAccessTokenAsync(); @@ -207,5 +253,13 @@ public async Task DeclineFriendRequestAsync(Guid requestId, Guid requesterId, Gu var payload = new { RequesterId = requesterId, FriendId = friendId }; await _httpClient.PostAsync($"friends/requests/{requestId}/decline", payload); } + + public async Task WithdrawFriendRequestAsync(Guid inviterId, Guid inviteeId) + { + string accessToken = await _identityService.GetAccessTokenAsync(); + _httpClient.SetAccessToken(accessToken); + var payload = new { InviterId = inviterId, InviteeId = inviteeId }; + await _httpClient.PutAsync($"friends/requests/{inviteeId}/withdraw", payload); + } } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Friends/IFriendsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Friends/IFriendsService.cs index 7858e686..7ed70bbc 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Friends/IFriendsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Friends/IFriendsService.cs @@ -20,9 +20,10 @@ public interface IFriendsService Task> GetAllStudentsAsync(); Task> GetAllStudentsAsync(int page = 1, int resultsPerPage = 10); Task InviteStudent(Guid inviterId, Guid inviteeId); - Task> GetSentFriendRequestsAsync(); + Task> GetSentFriendRequestsAsync(); Task> GetIncomingFriendRequestsAsync(); Task AcceptFriendRequestAsync(Guid requestId, Guid requesterId, Guid friendId); Task DeclineFriendRequestAsync(Guid requestId, Guid requesterId, Guid friendId); + Task WithdrawFriendRequestAsync(Guid inviterId, Guid inviteeId); } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/MediaFiles/IMediaFilesService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/MediaFiles/IMediaFilesService.cs new file mode 100644 index 00000000..10de2123 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/MediaFiles/IMediaFilesService.cs @@ -0,0 +1,16 @@ +using System; +using System.Threading.Tasks; +using MiniSpace.Web.DTO; +using MiniSpace.Web.HttpClients; + +namespace MiniSpace.Web.Areas.MediaFiles +{ + public interface IMediaFilesService + { + public Task GetFileAsync(Guid fileId); + public Task GetOriginalFileAsync(Guid fileId); + public Task> UploadMediaFileAsync(Guid sourceId, string sourceType, + Guid uploaderId, string fileName, string fileContentType, string base64Content); + public Task DeleteMediaFileAsync(Guid fileId); + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/MediaFiles/MediaFilesService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/MediaFiles/MediaFilesService.cs new file mode 100644 index 00000000..c3437143 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/MediaFiles/MediaFilesService.cs @@ -0,0 +1,46 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using MiniSpace.Web.Areas.Identity; +using MiniSpace.Web.DTO; +using MiniSpace.Web.HttpClients; + +namespace MiniSpace.Web.Areas.MediaFiles +{ + public class MediaFilesService : IMediaFilesService + { + private readonly IHttpClient _httpClient; + private readonly IIdentityService _identityService; + + public MediaFilesService(IHttpClient httpClient, IIdentityService identityService) + { + _httpClient = httpClient; + _identityService = identityService; + } + + public Task GetFileAsync(Guid fileId) + { + return _httpClient.GetAsync($"media-files/{fileId}"); + } + + public Task GetOriginalFileAsync(Guid fileId) + { + return _httpClient.GetAsync($"media-files/{fileId}/original"); + } + + public Task> UploadMediaFileAsync(Guid sourceId, string sourceType, Guid uploaderId, string fileName, + string fileContentType, string base64Content) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return _httpClient.PostAsync("media-files", new {sourceId, sourceType, uploaderId, + fileName, fileContentType, base64Content }); + } + + public Task DeleteMediaFileAsync(Guid fileId) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return _httpClient.DeleteAsync($"media-files/{fileId}"); + } + + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Notifications/INotificationsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Notifications/INotificationsService.cs new file mode 100644 index 00000000..63a46917 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Notifications/INotificationsService.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Web.DTO; +using MiniSpace.Web.DTO.Notifications; + +namespace MiniSpace.Web.Areas.Notifications +{ + public interface INotificationsService + { + Task> GetNotificationsByUserAsync(Guid userId, int page = 1, int pageSize = 10, string sortOrder = "desc"); + Task> GetNotificationsByUserAsync(Guid userId, int page = 1, int pageSize = 10, string sortOrder = "desc", string status = "Unread"); + + Task UpdateNotificationStatusAsync(Guid userId, Guid notificationId, string status); + + Task UpdateNotificationStatusAsync(Guid notificationId, bool isActive); + Task DeleteNotificationAsync(Guid userId, Guid notificationId); + Task GetNotificationByIdAsync(Guid userId, Guid notificationId); + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Notifications/NotificationsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Notifications/NotificationsService.cs new file mode 100644 index 00000000..9f1dfe19 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Notifications/NotificationsService.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Web.Areas.Identity; +using MiniSpace.Web.DTO; +using MiniSpace.Web.Data.Events; +using MiniSpace.Web.DTO.Wrappers; +using MiniSpace.Web.HttpClients; +using Blazorise; +using MiniSpace.Web.DTO.Notifications; + +namespace MiniSpace.Web.Areas.Notifications +{ + public class NotificationsService: INotificationsService + { + private readonly IHttpClient _httpClient; + private readonly IIdentityService _identityService; + + public NotificationsService(IHttpClient httpClient, IIdentityService identityService) + { + _httpClient = httpClient; + _identityService = identityService; + } + + public async Task> GetNotificationsByUserAsync(Guid userId, int page = 1, int pageSize = 10, string sortOrder = "desc") + { + string accessToken = await _identityService.GetAccessTokenAsync(); + _httpClient.SetAccessToken(accessToken); + var url = $"notifications/{userId}?page={page}&pageSize={pageSize}&sortOrder={sortOrder}"; + + // Fetch the paginated response from the server + var response = await _httpClient.GetAsync>(url); + + return response; + } + + // public async Task CreateNotificationAsync(NotificationDto notification) + // { + // string accessToken = await _identityService.GetAccessTokenAsync(); + // _httpClient.SetAccessToken(accessToken); + // return await _httpClient.PostAsync("notifications", notification); + // } + + public async Task UpdateNotificationStatusAsync(Guid userId, Guid notificationId, string status) + { + string accessToken = await _identityService.GetAccessTokenAsync(); + _httpClient.SetAccessToken(accessToken); + var payload = new { UserId = userId, NotificationId = notificationId, Status = status }; + var url = $"notifications/{userId}/{notificationId}/status"; + await _httpClient.PutAsync(url, payload); + } + + public async Task UpdateNotificationStatusAsync(Guid notificationId, bool isActive) + { + string accessToken = await _identityService.GetAccessTokenAsync(); + _httpClient.SetAccessToken(accessToken); + await _httpClient.PutAsync($"notifications/{notificationId}/status", new { IsActive = isActive }); + } + + public async Task DeleteNotificationAsync(Guid userId, Guid notificationId) + { + string accessToken = await _identityService.GetAccessTokenAsync(); + var payload = new { UserId = userId, NotificationId = notificationId}; + _httpClient.SetAccessToken(accessToken); + var url = $"notifications/notification/{userId}/{notificationId}"; + await _httpClient.DeleteAsync(url, payload); + } + + + public async Task GetNotificationByIdAsync(Guid userId, Guid notificationId) + { + string accessToken = await _identityService.GetAccessTokenAsync(); + _httpClient.SetAccessToken(accessToken); + var url = $"notifications/{userId}/{notificationId}"; + return await _httpClient.GetAsync(url); + } + + public async Task> GetNotificationsByUserAsync(Guid userId, int page = 1, int pageSize = 20, string sortOrder = "desc", string status = "Unread") + { + string accessToken = await _identityService.GetAccessTokenAsync(); + _httpClient.SetAccessToken(accessToken); + var url = $"notifications/{userId}?page={page}&pageSize={pageSize}&sortOrder={sortOrder}&status={status}"; + + var response = await _httpClient.GetAsync>(url); + + return response; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/IOrganizationsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/IOrganizationsService.cs index cefb8d31..9e2f6f14 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/IOrganizationsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/IOrganizationsService.cs @@ -10,13 +10,15 @@ namespace MiniSpace.Web.Areas.Organizations { public interface IOrganizationsService { - Task GetOrganizationAsync(Guid organizationId); - Task GetOrganizationDetailsAsync(Guid organizationId); + Task GetOrganizationAsync(Guid organizationId, Guid rootId); + Task GetOrganizationDetailsAsync(Guid organizationId, Guid rootId); Task> GetOrganizerOrganizationsAsync(Guid organizerId); Task> GetRootOrganizationsAsync(); - Task> GetChildrenOrganizationsAsync(Guid organizationId); - Task> AddOrganization(Guid organizationId, string name, Guid parentId); - Task AddOrganizerToOrganization(Guid organizationId, Guid organizerId); - Task RemoveOrganizerFromOrganization(Guid organizationId, Guid organizerId); + Task> GetChildrenOrganizationsAsync(Guid organizationId, Guid rootId); + Task> GetAllChildrenOrganizationsAsync(Guid organizationId, Guid rootId); + Task> CreateOrganization(Guid organizationId, string name, Guid rootId, Guid parentId); + Task> CreateRootOrganization(Guid organizationId, string name); + Task AddOrganizerToOrganization(Guid rootOrganizationId, Guid organizationId, Guid organizerId); + Task RemoveOrganizerFromOrganization(Guid rootOrganizationId, Guid organizationId, Guid organizerId); } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/OrganizationsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/OrganizationsService.cs index f447a01f..0e53c224 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/OrganizationsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/OrganizationsService.cs @@ -18,16 +18,16 @@ public OrganizationsService(IHttpClient httpClient, IIdentityService identitySer _identityService = identityService; } - public Task GetOrganizationAsync(Guid organizationId) + public Task GetOrganizationAsync(Guid organizationId, Guid rootId) { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); - return _httpClient.GetAsync($"organizations/{organizationId}"); + return _httpClient.GetAsync($"organizations/{organizationId}?rootId={rootId}"); } - public Task GetOrganizationDetailsAsync(Guid organizationId) + public Task GetOrganizationDetailsAsync(Guid organizationId, Guid rootId) { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); - return _httpClient.GetAsync($"organizations/{organizationId}/details"); + return _httpClient.GetAsync($"organizations/{organizationId}/details?rootId={rootId}"); } public Task> GetOrganizerOrganizationsAsync(Guid organizerId) @@ -42,30 +42,44 @@ public Task> GetRootOrganizationsAsync() return _httpClient.GetAsync>("organizations/root"); } - public Task> GetChildrenOrganizationsAsync(Guid organizationId) + public Task> GetChildrenOrganizationsAsync(Guid organizationId, Guid rootId) { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); return _httpClient.GetAsync> - ($"organizations/{organizationId}/children?parentId={organizationId}"); + ($"organizations/{organizationId}/children?rootId={rootId}"); } - public Task> AddOrganization(Guid organizationId, string name, Guid parentId) + public Task> GetAllChildrenOrganizationsAsync(Guid organizationId, Guid rootId) { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); - return _httpClient.PostAsync("organizations", new {organizationId, name, parentId}); + return _httpClient.GetAsync> + ($"organizations/{organizationId}/children/all?rootId={rootId}"); } - public Task AddOrganizerToOrganization(Guid organizationId, Guid organizerId) + public Task> CreateOrganization(Guid organizationId, string name, Guid rootId, Guid parentId) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return _httpClient.PostAsync($"organizations/{organizationId}/children", + new {organizationId, name, rootId, parentId}); + } + + public Task> CreateRootOrganization(Guid organizationId, string name) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return _httpClient.PostAsync("organizations", new {organizationId, name}); + } + + public Task AddOrganizerToOrganization(Guid rootOrganizationId, Guid organizationId, Guid organizerId) { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); return _httpClient.PostAsync($"organizations/{organizationId}/organizer", - new {organizationId, organizerId}); + new {rootOrganizationId, organizationId, organizerId}); } - public Task RemoveOrganizerFromOrganization(Guid organizationId, Guid organizerId) + public Task RemoveOrganizerFromOrganization(Guid rootOrganizationId, Guid organizationId, Guid organizerId) { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); - return _httpClient.DeleteAsync($"organizations/{organizationId}/organizer/{organizerId}"); + return _httpClient.DeleteAsync($"organizations/{organizationId}/organizer/{organizerId}?rootOrganizationId={rootOrganizationId}"); } } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Posts/IPostsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Posts/IPostsService.cs index 81ce84a8..d89577bc 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Posts/IPostsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Posts/IPostsService.cs @@ -11,9 +11,9 @@ public interface IPostsService Task GetPostAsync(Guid postId); Task ChangePostStateAsync(Guid postId, string state, DateTime publishDate); Task> CreatePostAsync(Guid postId, Guid eventId, Guid organizerId, string textContext, - string mediaContext, string state, DateTime? publishDate); + IEnumerable mediaFiles, string state, DateTime? publishDate); Task DeletePostAsync(Guid postId); Task> GetPostsAsync(Guid eventId); - Task> UpdatePostAsync(Guid postId, string textContent, string mediaContent); + Task> UpdatePostAsync(Guid postId, string textContent, IEnumerable mediaFiles); } } \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Posts/PostsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Posts/PostsService.cs index 541ad86b..1b575c05 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Posts/PostsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Posts/PostsService.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Threading.Tasks; using MiniSpace.Web.Areas.Identity; @@ -30,11 +31,11 @@ public Task ChangePostStateAsync(Guid postId, string state, DateTime publishDate } public Task> CreatePostAsync(Guid postId, Guid eventId, Guid organizerId, string textContent, - string mediaContext, string state, DateTime? publishDate) + IEnumerable mediaFiles, string state, DateTime? publishDate) { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); return _httpClient.PostAsync("posts", new {postId, eventId, organizerId, textContent, - mediaContext, state, publishDate}); + mediaFiles, state, publishDate}); } public Task DeletePostAsync(Guid postId) @@ -48,10 +49,10 @@ public Task> GetPostsAsync(Guid eventId) return _httpClient.GetAsync>($"posts?eventId={eventId}"); } - public Task> UpdatePostAsync(Guid postId, string textContent, string mediaContent) + public Task> UpdatePostAsync(Guid postId, string textContent, IEnumerable mediaFiles) { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); - return _httpClient.PutAsync($"posts/{postId}", new {postId, textContent, mediaContent}); + return _httpClient.PutAsync($"posts/{postId}", new {postId, textContent, mediaFiles}); } } } \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Reactions/IReactionsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Reactions/IReactionsService.cs new file mode 100644 index 00000000..88adde6b --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Reactions/IReactionsService.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Web.DTO; +using MiniSpace.Web.DTO.Enums; +using MiniSpace.Web.HttpClients; + +namespace MiniSpace.Web.Areas.Reactions +{ + public interface IReactionsService + { + Task> GetReactions(Guid contentId, ReactionContentType contentType); + Task GetReactionsSummary(Guid contentId, ReactionContentType contentType); + Task> CreateReaction(Guid reactionId, Guid studentId, string reactionType, + Guid contentId, string contentType); + Task DeleteReaction(Guid reactionId); + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Reactions/ReactionsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Reactions/ReactionsService.cs new file mode 100644 index 00000000..4aae4aad --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Reactions/ReactionsService.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Web.Areas.Identity; +using MiniSpace.Web.DTO; +using MiniSpace.Web.DTO.Enums; +using MiniSpace.Web.HttpClients; + +namespace MiniSpace.Web.Areas.Reactions +{ + public class ReactionsService : IReactionsService + { + private readonly IHttpClient _httpClient; + private readonly IIdentityService _identityService; + + public ReactionsService(IHttpClient httpClient, IIdentityService identityService) + { + _httpClient = httpClient; + _identityService = identityService; + } + + public Task> GetReactions(Guid contentId, ReactionContentType contentType) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return _httpClient.GetAsync>($"reactions?contentId={contentId}&contentType={contentType}"); + } + + public Task GetReactionsSummary(Guid contentId, ReactionContentType contentType) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return _httpClient.GetAsync($"reactions/summary?contentId={contentId}&contentType={contentType}"); + } + + public Task> CreateReaction(Guid reactionId, Guid studentId, string reactionType, + Guid contentId, string contentType) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return _httpClient.PostAsync("reactions", + new { reactionId, studentId, reactionType, contentId, contentType }); + } + + public Task DeleteReaction(Guid reactionId) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return _httpClient.DeleteAsync($"reactions/{reactionId}"); + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/IStudentsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/IStudentsService.cs index d6a8e8c0..0514d4e0 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/IStudentsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/IStudentsService.cs @@ -12,9 +12,9 @@ public interface IStudentsService Task UpdateStudentDto(Guid studentId); void ClearStudentDto(); Task GetStudentAsync(Guid studentId); - Task> GetStudentsAsync(); - Task UpdateStudentAsync(Guid studentId, string profileImage, string description, bool emailNotifications); - Task> CompleteStudentRegistrationAsync(Guid studentId, string profileImage, + Task> GetStudentsAsync(); + Task UpdateStudentAsync(Guid studentId, Guid profileImage, string description, bool emailNotifications); + Task> CompleteStudentRegistrationAsync(Guid studentId, Guid profileImage, string description, DateTime dateOfBirth, bool emailNotifications); Task GetStudentStateAsync(Guid studentId); } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/StudentsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/StudentsService.cs index 2d1afeda..d0757b6e 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/StudentsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/StudentsService.cs @@ -39,20 +39,20 @@ public async Task GetStudentAsync(Guid studentId) return await _httpClient.GetAsync($"students/{studentId}"); } - public Task> GetStudentsAsync() + public Task> GetStudentsAsync() { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); - return _httpClient.GetAsync>("students"); + return _httpClient.GetAsync>("students"); } - public Task UpdateStudentAsync(Guid studentId, string profileImage, string description, bool emailNotifications) + public Task UpdateStudentAsync(Guid studentId, Guid profileImage, string description, bool emailNotifications) { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); return _httpClient.PutAsync($"students/{studentId}", new {studentId, profileImage, description, emailNotifications}); } - public Task> CompleteStudentRegistrationAsync(Guid studentId, string profileImage, + public Task> CompleteStudentRegistrationAsync(Guid studentId, Guid profileImage, string description, DateTime dateOfBirth, bool emailNotifications) => _httpClient.PostAsync("students", new {studentId, profileImage, description, dateOfBirth, emailNotifications}); diff --git a/MiniSpace.Web/src/MiniSpace.Web/Components/RadzenEventCard.razor b/MiniSpace.Web/src/MiniSpace.Web/Components/RadzenEventCard.razor new file mode 100644 index 00000000..24e89a8a --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Components/RadzenEventCard.razor @@ -0,0 +1,46 @@ +@using MiniSpace.Web.DTO +@inject NavigationManager NavigationManager + + + + + Name + @(Event.Name) + + + Status + @switch (Event.Status) + { + case "Published": + @Event.Status + break; + case "Archived": + @Event.Status + break; + default: + @Event.Status + break; + } + + + + + Start date + @(Event.StartDate.ToLocalTime().ToString(dateFormat)) + + + End date + @(Event.EndDate.ToLocalTime().ToString(dateFormat)) + + +
+
+ +@code +{ + [Parameter] + public EventDto Event { get; set; } + + private const string dateFormat = "dd/MM/yyyy HH:mm"; +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/Enums/NotificationEventType.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Enums/NotificationEventType.cs new file mode 100644 index 00000000..d258a617 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Enums/NotificationEventType.cs @@ -0,0 +1,13 @@ +namespace MiniSpace.Web.DTO.Enums +{ + public enum NotificationEventType + { + NewFriendRequest, + NewPost, + NewEvent, + FriendRequestAccepted, + MentionedInPost, + EventReminder, + Other + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/Enums/ReactionContentType.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Enums/ReactionContentType.cs new file mode 100644 index 00000000..583f4166 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Enums/ReactionContentType.cs @@ -0,0 +1,8 @@ +namespace MiniSpace.Web.DTO.Enums +{ + public enum ReactionContentType + { + Event, + Post + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/Enums/ReactionType.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Enums/ReactionType.cs new file mode 100644 index 00000000..dcc9637d --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Enums/ReactionType.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; + +namespace MiniSpace.Web.DTO.Enums +{ + public enum ReactionType + { + LoveIt, + LikeIt, + Wow, + ItWasOkay, + HateIt + } + + public static class ReactionTypeExtensions + { + public static string GetReactionText(ReactionType? reactionType) + { + return reactionType switch + { + ReactionType.LoveIt => "Love it!", + ReactionType.LikeIt => "Like it.", + ReactionType.Wow => "Wow!", + ReactionType.ItWasOkay => "It was okay.", + ReactionType.HateIt => "Hate it!", + _ => "No reactions!" + }; + } + + public static List> GenerateReactionPairs() + => [ + new KeyValuePair("", null), + new KeyValuePair(GetReactionText(ReactionType.LoveIt), ReactionType.LoveIt), + new KeyValuePair(GetReactionText(ReactionType.LikeIt), ReactionType.LikeIt), + new KeyValuePair(GetReactionText(ReactionType.Wow), ReactionType.Wow), + new KeyValuePair(GetReactionText(ReactionType.ItWasOkay), ReactionType.ItWasOkay), + new KeyValuePair(GetReactionText(ReactionType.HateIt), ReactionType.HateIt) + ]; + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/EventDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/EventDto.cs index 4e4f40d1..de41f57d 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/DTO/EventDto.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/EventDto.cs @@ -12,6 +12,7 @@ public class EventDto public DateTime StartDate { get; set; } public DateTime EndDate { get; set; } public AddressDto Location { get; set; } + public IEnumerable MediaFiles { get; set; } public int InterestedStudents { get; set; } public int SignedUpStudents { get; set; } public int Capacity { get; set; } diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/FileDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/FileDto.cs new file mode 100644 index 00000000..7b2075b5 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/FileDto.cs @@ -0,0 +1,17 @@ +using System; + +namespace MiniSpace.Web.DTO +{ + public class FileDto + { + public Guid MediaFileId { get; set; } + public Guid SourceId { get; set; } + public string SourceType { get; set; } + public string State { get; set; } + public DateTime CreatedAt { get; set; } + public Guid UploaderId { get; set; } + public string FileName { get; set; } + public string FileContentType { get; set; } + public string Base64Content { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/FileUploadResponseDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/FileUploadResponseDto.cs new file mode 100644 index 00000000..cfb4804c --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/FileUploadResponseDto.cs @@ -0,0 +1,9 @@ +using System; + +namespace MiniSpace.Web.DTO +{ + public class FileUploadResponseDto + { + public Guid FileId { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/FriendRequestDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/FriendRequestDto.cs index a39262bc..6dd7ce91 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/DTO/FriendRequestDto.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/FriendRequestDto.cs @@ -13,7 +13,10 @@ public class FriendRequestDto public Guid StudentId { get; set; } public string InviteeName { get; set; } + public string InviterName { get; set; } public string InviteeEmail { get; set; } public string InviteeImage { get; set; } + public string InviterEmail { get; set; } + public string InviterImage { get; set; } } } \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/Notifications/NotificationDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Notifications/NotificationDto.cs new file mode 100644 index 00000000..f1f65121 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Notifications/NotificationDto.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using MiniSpace.Web.DTO.Enums; + +namespace MiniSpace.Web.DTO.Notifications +{ + public class NotificationDto + { + public Guid NotificationId { get; set; } + public Guid UserId { get; set; } + public string Message { get; set; } + public string Status { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime? UpdatedAt { get; set; } + public Guid? RelatedEntityId { get; set; } + public NotificationEventType EventType { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/OrganizationDetailsDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/OrganizationDetailsDto.cs index dfaa9bd9..e8afa161 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/DTO/OrganizationDetailsDto.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/OrganizationDetailsDto.cs @@ -7,8 +7,7 @@ public class OrganizationDetailsDto { public Guid Id { get; set; } public string Name { get; set; } - public Guid ParentId { get; set; } - public bool IsLeaf { get; set; } + public Guid RootId { get; set; } public IEnumerable Organizers { get; set; } } } \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/OrganizationDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/OrganizationDto.cs index 91138784..8526582f 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/DTO/OrganizationDto.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/OrganizationDto.cs @@ -6,7 +6,6 @@ public class OrganizationDto { public Guid Id { get; set; } public string Name { get; set; } - public Guid ParentId { get; set; } - public bool IsLeaf { get; set; } + public Guid RootId { get; set; } } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/PostDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/PostDto.cs index 96646171..47961054 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/DTO/PostDto.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/PostDto.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace MiniSpace.Web.DTO { @@ -8,7 +9,7 @@ public class PostDto public Guid EventId { get; set; } public Guid OrganizerId { get; set; } public string TextContent { get; set; } - public string MediaContent { get; set; } + public IEnumerable MediaFiles { get; set; } public string State { get; set; } public DateTime? PublishDate { get; set; } public DateTime CreatedAt { get; set; } diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/ReactionDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/ReactionDto.cs new file mode 100644 index 00000000..91068138 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/ReactionDto.cs @@ -0,0 +1,15 @@ +using System; +using MiniSpace.Web.DTO.Enums; + +namespace MiniSpace.Web.DTO +{ + public class ReactionDto + { + public Guid Id { get; set; } + public Guid StudentId { get; set; } + public string StudentFullName { get; set; } + public Guid ContentId { get; set; } + public ReactionContentType ContentType { get; set; } + public ReactionType Type { get; set; } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/ReactionsSummaryDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/ReactionsSummaryDto.cs new file mode 100644 index 00000000..6e3cfbef --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/ReactionsSummaryDto.cs @@ -0,0 +1,13 @@ +using System; +using MiniSpace.Web.DTO.Enums; + +namespace MiniSpace.Web.DTO +{ + public class ReactionsSummaryDto + { + public int NumberOfReactions { get; set; } + public ReactionType? DominantReaction { get; set; } + public Guid? AuthUserReactionId { get; set; } + public ReactionType? AuthUserReactionType { get; set; } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/States/FriendState.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/States/FriendState.cs index 5ccd31ad..1446eed7 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/DTO/States/FriendState.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/States/FriendState.cs @@ -2,12 +2,13 @@ namespace MiniSpace.Web.DTO.States { public enum FriendState { - Unknown, - Requested, + Unknown, + Requested, Accepted, - Declined, - Blocked, + Declined, + Blocked, Cancelled, - Confirmed + Confirmed, + Pending } } \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/StudentDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/StudentDto.cs index 0400d6cd..a09c5292 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/DTO/StudentDto.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/StudentDto.cs @@ -10,7 +10,7 @@ public class StudentDto public string FirstName { get; set; } public string LastName { get; set; } public int NumberOfFriends { get; set; } - public string ProfileImage { get; set; } + public Guid ProfileImage { get; set; } public string Description { get; set; } public DateTime DateOfBirth { get; set; } public bool EmailNotifications { get; set; } diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/StudentFriendsDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/StudentFriendsDto.cs new file mode 100644 index 00000000..f12f8d30 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/StudentFriendsDto.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MiniSpace.Web.DTO +{ + public class StudentFriendsDto + { + public Guid StudentId { get; set; } + public List Friends { get; set; } = new List(); + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/StudentRequestsDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/StudentRequestsDto.cs new file mode 100644 index 00000000..501844fb --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/StudentRequestsDto.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using MiniSpace.Services.Friends.Application.Dto; + +namespace MiniSpace.Web.DTO +{ + public class StudentRequestsDto + { + public Guid Id { get; set; } + public Guid StudentId { get; set; } + public List FriendRequests { get; set; } = new List(); + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/Types/MediaFileContextType.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Types/MediaFileContextType.cs new file mode 100644 index 00000000..9cbe6b2e --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Types/MediaFileContextType.cs @@ -0,0 +1,9 @@ +namespace MiniSpace.Web.DTO.Types +{ + public enum MediaFileContextType + { + Event, + Post, + StudentProfile, + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/Wrappers/EventParticipantsDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Wrappers/EventParticipantsDto.cs new file mode 100644 index 00000000..6263d516 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Wrappers/EventParticipantsDto.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; + +namespace MiniSpace.Web.DTO.Wrappers +{ + public class EventParticipantsDto + { + public Guid EventId { get; set; } + public IEnumerable InterestedStudents { get; set; } + public IEnumerable SignedUpStudents { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Data/Events/SearchEvents.cs b/MiniSpace.Web/src/MiniSpace.Web/Data/Events/SearchEvents.cs index ad9d1916..4cc8511b 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Data/Events/SearchEvents.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Data/Events/SearchEvents.cs @@ -8,6 +8,8 @@ public class SearchEvents { public string Name { get; set; } public string Organizer { get; set; } + public Guid OrganizationId { get; set; } + public Guid RootOrganizationId { get; set; } public string Category { get; set; } public string State { get; set; } public IEnumerable Friends { get; set; } @@ -16,11 +18,14 @@ public class SearchEvents public string DateTo { get; set; } public PageableDto Pageable { get; set; } - public SearchEvents(string name, string organizer, string category, string state, IEnumerable friends, - string friendsEngagementType, string dateFrom, string dateTo, PageableDto pageable) + public SearchEvents(string name, string organizer, Guid organizationId, Guid rootOrganizationId, + string category, string state, IEnumerable friends, string friendsEngagementType, + string dateFrom, string dateTo, PageableDto pageable) { Name = name; Organizer = organizer; + OrganizationId = organizationId; + RootOrganizationId = rootOrganizationId; Category = category; State = state; Friends = friends; diff --git a/MiniSpace.Web/src/MiniSpace.Web/HttpClients/CustomHttpClient.cs b/MiniSpace.Web/src/MiniSpace.Web/HttpClients/CustomHttpClient.cs index 90d065e5..c6f01fa8 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/HttpClients/CustomHttpClient.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/HttpClients/CustomHttpClient.cs @@ -78,18 +78,18 @@ public Task DeleteAsync(string uri) public async Task DeleteAsync(string uri, object payload) { var jsonPayload = JsonConvert.SerializeObject(payload, JsonSerializerSettings); - _logger.LogDebug($"Sending HTTP DELETE request to URI: {uri} with payload: {jsonPayload}"); + _logger.LogInformation($"Sending HTTP DELETE request to URI: {uri} with payload: {jsonPayload}"); var request = new HttpRequestMessage(HttpMethod.Delete, uri) { - Content = new StringContent(jsonPayload, Encoding.UTF8, "application/json") + Content = new StringContent(jsonPayload, Encoding.UTF8, "text/plain") }; var response = await _client.SendAsync(request); if (!response.IsSuccessStatusCode) { var errorContent = await response.Content.ReadAsStringAsync(); - _logger.LogError($"Error response from server: {errorContent}"); + _logger.LogInformation($"Error response from server: {errorContent}"); throw new HttpRequestException($"Request to {uri} failed with status code {response.StatusCode} and message {errorContent}"); } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/MiniSpace.Web.csproj b/MiniSpace.Web/src/MiniSpace.Web/MiniSpace.Web.csproj index 1df37b7d..de79ee77 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/MiniSpace.Web.csproj +++ b/MiniSpace.Web/src/MiniSpace.Web/MiniSpace.Web.csproj @@ -13,7 +13,7 @@ - + diff --git a/MiniSpace.Web/src/MiniSpace.Web/Models/Events/AddEventModel.cs b/MiniSpace.Web/src/MiniSpace.Web/Models/Events/CreateEventModel.cs similarity index 79% rename from MiniSpace.Web/src/MiniSpace.Web/Models/Events/AddEventModel.cs rename to MiniSpace.Web/src/MiniSpace.Web/Models/Events/CreateEventModel.cs index 8e79c5b1..f37f7006 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Models/Events/AddEventModel.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Models/Events/CreateEventModel.cs @@ -1,13 +1,15 @@ using System; +using System.Collections.Generic; +using MiniSpace.Web.DTO; namespace MiniSpace.Web.Models.Events { - public class AddEventModel + public class CreateEventModel { public Guid EventId { get; set; } public string Name { get; set; } public Guid OrganizerId { get; set; } - public Guid OrganizationId { get; set; } + public OrganizationDto Organization { get; set; } public DateTime StartDate { get; set; } public DateTime EndDate { get; set; } public string BuildingName { get; set; } @@ -16,6 +18,7 @@ public class AddEventModel public string ApartmentNumber { get; set; } public string City { get; set; } public string ZipCode { get; set; } + public IEnumerable MediaFiles { get; } public string Description { get; set; } public int Capacity { get; set; } public decimal Fee { get; set; } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Models/Events/SearchEventsModel.cs b/MiniSpace.Web/src/MiniSpace.Web/Models/Events/SearchEventsModel.cs index a4e18721..727644ea 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Models/Events/SearchEventsModel.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Models/Events/SearchEventsModel.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using MiniSpace.Web.DTO.Wrappers; +using MiniSpace.Web.Models.Organizations; namespace MiniSpace.Web.Models.Events { @@ -8,6 +9,7 @@ public class SearchEventsModel { public string Name { get; set; } public string Organizer { get; set; } + public OrganizationModel Organization { get; set; } public string Category { get; set; } public string State { get; set; } public IEnumerable Friends { get; set; } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Models/Events/UpdateEventModel.cs b/MiniSpace.Web/src/MiniSpace.Web/Models/Events/UpdateEventModel.cs index f25a8a73..fcc96338 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Models/Events/UpdateEventModel.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Models/Events/UpdateEventModel.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace MiniSpace.Web.Models.Events { @@ -15,6 +16,7 @@ public class UpdateEventModel public string ApartmentNumber { get; set; } public string City { get; set; } public string ZipCode { get; set; } + public IEnumerable MediaFiles { get; set; } public string Description { get; set; } public int Capacity { get; set; } public decimal Fee { get; set; } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Models/Organizations/OrganizationModel.cs b/MiniSpace.Web/src/MiniSpace.Web/Models/Organizations/OrganizationModel.cs new file mode 100644 index 00000000..624f6bf7 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Models/Organizations/OrganizationModel.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; + +namespace MiniSpace.Web.Models.Organizations +{ + public class OrganizationModel + { + public Guid Id { get; set; } + public string Name { get; set; } + public Guid RootId { get; set; } + public List Children { get; set; } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Models/Organizations/OrganizerModel.cs b/MiniSpace.Web/src/MiniSpace.Web/Models/Organizations/OrganizerModel.cs new file mode 100644 index 00000000..52d26336 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Models/Organizations/OrganizerModel.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; + +namespace MiniSpace.Web.Models.Organizations +{ + public class OrganizerModel + { + public Guid Id { get; set; } + public string Email { get; set; } + public string Name { get; set; } + public bool WasBelonging { get; set; } + + public OrganizerModel(Guid id, string email, string name) + { + Id = id; + Email = email; + Name = name; + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Models/Posts/CreatePostModel.cs b/MiniSpace.Web/src/MiniSpace.Web/Models/Posts/CreatePostModel.cs index d2316ace..eb3da90e 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Models/Posts/CreatePostModel.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Models/Posts/CreatePostModel.cs @@ -1,4 +1,6 @@ using System; +using System.Collections; +using System.Collections.Generic; namespace MiniSpace.Web.Models.Posts { @@ -8,7 +10,7 @@ public class CreatePostModel public Guid EventId { get; set; } public Guid OrganizerId { get; set; } public string TextContent { get; set; } - public string MediaContent { get; set; } + public IEnumerable MediaFiles { get; set; } public string State { get; set; } public DateTime PublishDate { get; set; } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Models/Posts/UpdatePostModel.cs b/MiniSpace.Web/src/MiniSpace.Web/Models/Posts/UpdatePostModel.cs index 67992392..4762b1b4 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Models/Posts/UpdatePostModel.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Models/Posts/UpdatePostModel.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace MiniSpace.Web.Models.Posts { @@ -6,6 +7,6 @@ public class UpdatePostModel { public Guid PostId { get; set; } public string TextContent { get; set; } - public string MediaContent { get; set; } + public IEnumerable MediaFiles { get; set; } } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Models/Students/CompleteRegistrationModel.cs b/MiniSpace.Web/src/MiniSpace.Web/Models/Students/CompleteRegistrationModel.cs index 5938dfe6..b7b904a1 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Models/Students/CompleteRegistrationModel.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Models/Students/CompleteRegistrationModel.cs @@ -5,7 +5,7 @@ namespace MiniSpace.Web.Models.Students public class CompleteRegistrationModel { public Guid StudentId { get; set; } - public string ProfileImage { get; set; } + public Guid ProfileImage { get; set; } public string Description { get; set; } public DateTime DateOfBirth { get; set; } public bool EmailNotifications { get; set; } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/AboutApp.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/AboutApp.razor index 746f95cb..40892d4f 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/AboutApp.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/AboutApp.razor @@ -2,7 +2,7 @@
@@ -17,6 +17,7 @@
+ + + +
+
+ +
+
+
+ + + + + + + + + + + + + + + + + +
+
@code { private SignInModel signInModel = new SignInModel(); @@ -84,31 +160,30 @@ } *@ private async Task HandleSignIn() -{ - try { - var response = await IdentityService.SignInAsync(signInModel.Email, signInModel.Password); - if (response != null && response.Content != null && !string.IsNullOrEmpty(response.Content.AccessToken)) + try { - // Assuming UpdateStudentDto and other subsequent methods correctly handle null checks - await StudentsService.UpdateStudentDto(IdentityService.UserDto.Id); - var nextPage = StudentsService.StudentDto.State == "incomplete" ? "/signup/complete" : "/account"; - NavigationManager.NavigateTo(nextPage); + var response = await IdentityService.SignInAsync(signInModel.Email, signInModel.Password); + if (response != null && response.Content != null && !string.IsNullOrEmpty(response.Content.AccessToken)) + { + await StudentsService.UpdateStudentDto(IdentityService.UserDto.Id); + var nextPage = StudentsService.StudentDto.State == "incomplete" ? "/signup/complete" : "/account"; + // Force a page reload when navigating + NavigationManager.NavigateTo(nextPage, true); + } + else + { + showError = true; + errorMessage = "Invalid login attempt"; + StateHasChanged(); + } } - else + catch (Exception ex) { + showError = true; - errorMessage = "Invalid login attempt"; - StateHasChanged(); // Force the component to re-render + errorMessage = $"Error during sign in: {ex.Message}"; + StateHasChanged(); } } - catch (Exception ex) - { - // Log the exception or handle it accordingly - showError = true; - errorMessage = $"Error during sign in: {ex.Message}"; - StateHasChanged(); // Update UI to show error message - } -} - } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/SignUp.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/SignUp.razor index 200a6e8b..0f996e9a 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/SignUp.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/SignUp.razor @@ -1,93 +1,126 @@ @page "/signup" +@using MiniSpace.Web.Models.Identity @using MiniSpace.Web.Areas.Identity +@using MiniSpace.Web.Areas.Students @using Radzen @using MiniSpace.Web.Areas.Http @inject IIdentityService IdentityService +@inject IStudentsService StudentsService @inject IErrorMapperService ErrorMapperService @inject NavigationManager NavigationManager +@inject IJSRuntime JSRuntime -

Welcome to MiniSpace

-

Please sign up to create your account and explore our world to the full.

-
 
-

Sign Up

- -
- - @errorMessage - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + +
+
+ +
+
+
+

Welcome to MiniSpace

+

Please sign up to create your account and explore our world to the full.

+

Sign Up

+ + + + + + + + + + + + + + + + + + + + + + +
+
+ @code { private SignUpModel signUpModel = new SignUpModel(); private bool showError = false; private string errorMessage = string.Empty; private bool popup; - + private async Task HandleSignUp() { - var response = await IdentityService.SignUpAsync(signUpModel.FirstName, signUpModel.LastName, signUpModel.Email, - signUpModel.Password, "user"); + var response = await IdentityService.SignUpAsync(signUpModel.FirstName, signUpModel.LastName, signUpModel.Email, signUpModel.Password, "user"); if(response.ErrorMessage == null) NavigationManager.NavigateTo("/signin"); else @@ -96,4 +129,10 @@ errorMessage = ErrorMapperService.MapError(response.ErrorMessage); } } -} \ No newline at end of file + + @* protected override async Task OnAfterRenderAsync(bool firstRender) { + if (firstRender) { + await JSRuntime.InvokeVoidAsync("initializeVideoPlayer", "videos/video-component/video_1.mp4", "videos/video-component/video_2.mp4"); + } + } *@ +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/UpdateAccount.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/UpdateAccount.razor deleted file mode 100644 index 305743c0..00000000 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/UpdateAccount.razor +++ /dev/null @@ -1,59 +0,0 @@ -@page "/account/update" -@using MiniSpace.Web.Areas.Students -@using MiniSpace.Web.DTO -@inject IIdentityService IdentityService -@inject IStudentsService StudentsService -@inject NavigationManager NavigationManager - -

Update some data in your account:

- -@if (showError) -{ -
- Failed to sign in. Please check your credentials and try again. -
-} - - - - - - @if (studentDto.Id != Guid.Empty) - { -
- - -
- -
- - -
- - - } - else - { -

Loading...

- } -
- -@code { - private StudentDto studentDto = new(); - private bool showError = false; - - protected override async Task OnInitializedAsync() - { - if (IdentityService.IsAuthenticated) - { - studentDto = StudentsService.StudentDto; - } - } - - private async Task HandleUpdateStudent() - { - await StudentsService.UpdateStudentAsync(studentDto.Id, studentDto.ProfileImage, - studentDto.Description, studentDto.EmailNotifications); - NavigationManager.NavigateTo("/account"); - } -} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Admin/Dialogs/AddOrganizationDialog.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Admin/Dialogs/AddOrganizationDialog.razor index cb5c3adb..c1e954dc 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Admin/Dialogs/AddOrganizationDialog.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Admin/Dialogs/AddOrganizationDialog.razor @@ -1,7 +1,7 @@ @page "/admin/organizations/add" @using Radzen -@using MiniSpace.Web.DTO @using MiniSpace.Web.Areas.Organizations +@using MiniSpace.Web.Models.Organizations @inject IOrganizationsService OrganizationsService @inject NavigationManager NavigationManager @inject Radzen.DialogService DialogService @@ -12,7 +12,7 @@ - + @@ -35,10 +35,12 @@ @code { [Parameter] - public OrganizationDto NewOrganization { get; set; } + public OrganizationModel ParentOrganization { get; set; } [Parameter] public bool IsRootOrganization { get; set; } + private string newOrganizationName { get; set; } + protected override async Task OnInitializedAsync() { await base.OnInitializedAsync(); @@ -46,8 +48,15 @@ private async void HandleAddingNewOrganization() { - await OrganizationsService.AddOrganization(Guid.Empty, NewOrganization.Name, - IsRootOrganization ? Guid.Empty : NewOrganization.ParentId); + if (IsRootOrganization) + { + await OrganizationsService.CreateRootOrganization(Guid.Empty, newOrganizationName); + } + else + { + await OrganizationsService.CreateOrganization(Guid.Empty, newOrganizationName, + ParentOrganization.RootId, ParentOrganization.Id); + } DialogService.Close(true); } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Admin/Dialogs/ManageStudentDialog.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Admin/Dialogs/ManageStudentDialog.razor index 62d8380d..7050e4de 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Admin/Dialogs/ManageStudentDialog.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Admin/Dialogs/ManageStudentDialog.razor @@ -103,7 +103,7 @@ Profile image - @@ -142,7 +142,7 @@ Date of birth - @(StudentDto.DateOfBirth.ToLocalTime().ToString(dateFormat)) + @(StudentDto.DateOfBirth.ToLocalTime().ToString(shortDateFormat)) @@ -172,6 +172,7 @@ public StudentDto StudentDto { get; set; } private const string dateFormat = "dd/MM/yyyy HH:mm"; + private const string shortDateFormat = "dd/MM/yyyy"; protected override async Task OnInitializedAsync() { diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Admin/Dialogs/OrganizationDetailsDialog.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Admin/Dialogs/OrganizationDetailsDialog.razor index da04a5bc..d2473333 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Admin/Dialogs/OrganizationDetailsDialog.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Admin/Dialogs/OrganizationDetailsDialog.razor @@ -2,6 +2,7 @@ @using Radzen @using MiniSpace.Web.DTO @using MiniSpace.Web.Areas.Organizations +@using MiniSpace.Web.Models.Organizations @inject IOrganizationsService OrganizationsService @inject NavigationManager NavigationManager @inject Radzen.DialogService DialogService @@ -45,13 +46,26 @@ - + + + Organizers not belonging: + + + Organizers belonging: + + + + @@ -59,14 +73,67 @@ @code { [Parameter] - public Guid OrganizationId { get; set; } + public OrganizationModel Organization { get; set; } + [Parameter] + public IEnumerable Organizers { get; set; } + + private OrganizationDetailsDto OrganizationDetailsDto { get; set; } = new(); - public OrganizationDetailsDto OrganizationDetailsDto { get; set; } = new(); - private Guid selectedOrganizer; + private IEnumerable notBelongingOrganizers = []; + private IEnumerable belongingOrganizers = []; protected override async Task OnInitializedAsync() { - OrganizationDetailsDto = await OrganizationsService.GetOrganizationDetailsAsync(OrganizationId); await base.OnInitializedAsync(); + OrganizationDetailsDto = await OrganizationsService.GetOrganizationDetailsAsync(Organization.Id, + Organization.RootId); + + HashSet belongingGuids = OrganizationDetailsDto.Organizers.ToHashSet(); + List notBelongingTmp = new(); + List belongingTmp = new(); + + foreach (var organizer in Organizers) + { + if (belongingGuids.Contains(organizer.Id)) + { + organizer.WasBelonging = true; + belongingTmp.Add(organizer); + } + else + { + organizer.WasBelonging = false; + notBelongingTmp.Add(organizer); + } + } + + notBelongingOrganizers = notBelongingTmp; + belongingOrganizers = belongingTmp; + } + + private async void HandleSubmitting() + { + if (belongingOrganizers != null) + { + foreach (var organizer in belongingOrganizers) + { + if (!organizer.WasBelonging) + { + await OrganizationsService.AddOrganizerToOrganization(Organization.RootId, Organization.Id, organizer.Id); + } + } + } + + if (notBelongingOrganizers != null) + { + foreach (var organizer in notBelongingOrganizers) + { + if (organizer.WasBelonging) + { + await OrganizationsService.RemoveOrganizerFromOrganization(Organization.RootId, Organization.Id, organizer.Id); + } + } + } + + DialogService.Close(true); } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Admin/ManageOrganizations.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Admin/ManageOrganizations.razor index 86630468..e9fd1ecf 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Admin/ManageOrganizations.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Admin/ManageOrganizations.razor @@ -2,6 +2,7 @@ @using MiniSpace.Web.Areas.Students @using MiniSpace.Web.DTO @using MiniSpace.Web.Areas.Organizations +@using MiniSpace.Web.Models.Organizations @using MiniSpace.Web.Pages.Admin.Dialogs @using Radzen @using DialogOptions = Radzen.DialogOptions @@ -9,6 +10,7 @@ @inject DialogService DialogService @inject IIdentityService IdentityService @inject IOrganizationsService OrganizationsService +@inject IStudentsService StudentsService @inject NavigationManager NavigationManager

Manage organizations

@@ -24,26 +26,26 @@ else + Click="@(() => OpenAddOrganizationDialog(selectedItem as OrganizationModel, true))" /> + Click="@(() => OpenAddOrganizationDialog(selectedItem as OrganizationModel, false))" /> + Click="@(() => OpenOrganizationDetailsDialog(selectedItem as OrganizationModel, organizers))" /> - + @if (totalRootOrganizations == 0) {

There are not any organizations created.

} - + @@ -56,78 +58,62 @@ else @code { private bool pageInitialized = false; - - public class ParentOrganization - { - public Guid Id { get; set; } - public string Name { get; set; } - public Guid ParentId { get; set; } - public bool IsLeaf { get; set; } - public List Children { get; set; } - } private int totalRootOrganizations = 0; - private List rootOrganizations = new(); + private List rootOrganizations = new(); + private object selectedItem; + + private IEnumerable organizers; protected override async Task OnInitializedAsync() { if (IdentityService.IsAuthenticated && IdentityService.GetCurrentUserRole() == "admin") { var tmpOrganizations = await OrganizationsService.GetRootOrganizationsAsync(); - ConvertOrganizationDtoList(tmpOrganizations, rootOrganizations); + ConvertOrganizationDtoList(tmpOrganizations, rootOrganizations, null); totalRootOrganizations = rootOrganizations.Count; } pageInitialized = true; + + if (IdentityService.IsAuthenticated && IdentityService.GetCurrentUserRole() == "admin") + { + var paginatedResponse = await StudentsService.GetStudentsAsync(); + organizers = paginatedResponse.Results + .Where(o => o.IsOrganizer) + .Select(s => new OrganizerModel(s.Id, s.Email, $"{s.FirstName} {s.LastName}")); + } } private async void OnExpand(TreeExpandEventArgs args) { - var parent = (ParentOrganization)args.Value; - var childOrganizations = await OrganizationsService.GetChildrenOrganizationsAsync(parent.Id); - ConvertOrganizationDtoList(childOrganizations, parent.Children); + var parent = (OrganizationModel)args.Value; + var childOrganizations = await OrganizationsService.GetChildrenOrganizationsAsync(parent.Id, parent.RootId); + ConvertOrganizationDtoList(childOrganizations, parent.Children, parent.RootId); StateHasChanged(); } - private static void ConvertOrganizationDtoList(IEnumerable input, IList result) + private static void ConvertOrganizationDtoList(IEnumerable input, IList result, Guid? rootId) { result.Clear(); foreach (var organization in input) { - result.Add(new ParentOrganization() + result.Add(new OrganizationModel() { Id = organization.Id, Name = organization.Name, - ParentId = organization.ParentId, - IsLeaf = organization.IsLeaf, - Children = new List() + RootId = rootId ?? organization.Id, + Children = new List() }); } } - - private bool HasChildren(object org) - { - var organization = (ParentOrganization)org; - return !organization.IsLeaf; - } - - private object selectedItem; - private OrganizationDto newOrganization = new(); - - private void OnChange() - { - if (selectedItem is ParentOrganization selectedOrganization) - { - newOrganization.ParentId = selectedOrganization.Id; - } - } - private async Task OpenAddOrganizationDialog(OrganizationDto newOrganization, bool isRootOrganization) + private async Task OpenAddOrganizationDialog(OrganizationModel parentOrganization, bool isRootOrganization) { - await DialogService.OpenAsync($"Add new organization:", + await DialogService.OpenAsync("Add new organization:", new Dictionary() { - { "NewOrganization", newOrganization }, + { "ParentOrganization", parentOrganization }, { "IsRootOrganization", isRootOrganization } }, new DialogOptions() @@ -140,18 +126,22 @@ else { await OnInitializedAsync(); } - else if (selectedItem is ParentOrganization selectedOrganization) + else if (selectedItem is OrganizationModel selectedOrg) { - var childOrganizations = await OrganizationsService.GetChildrenOrganizationsAsync(selectedOrganization.Id); - ConvertOrganizationDtoList(childOrganizations, selectedOrganization.Children); + var childOrganizations = await OrganizationsService.GetChildrenOrganizationsAsync(selectedOrg.Id, selectedOrg.RootId); + ConvertOrganizationDtoList(childOrganizations, selectedOrg.Children, selectedOrg.RootId); StateHasChanged(); } } - private async Task OpenOrganizationDetailsDialog(ParentOrganization organization) + private async Task OpenOrganizationDetailsDialog(OrganizationModel Organization, IEnumerable organizers) { - await DialogService.OpenAsync($"Details of the organization:", - new Dictionary() { { "OrganizationId", organization.Id } }, + await DialogService.OpenAsync("Details of the organization:", + new Dictionary() + { + { "Organization", Organization }, + { "Organizers", organizers } + }, new DialogOptions() { Width = "700px", Height = "600px", Resizable = true, Draggable = true, diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Admin/ManageStudents.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Admin/ManageStudents.razor index 2b404f30..e3ceee6c 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Admin/ManageStudents.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Admin/ManageStudents.razor @@ -44,8 +44,16 @@ - - + + + + + + @@ -54,6 +62,7 @@ @code { private const string dateFormat = "dd/MM/yyyy HH:mm"; + private const string shortDateFormat = "dd/MM/yyyy"; private int pageSize = 5; IEnumerable pageSizeOptions = new int[] { 5, 10, 20, 40}; @@ -70,7 +79,8 @@ { adminId = IdentityService.GetCurrentUserId(); - students = await StudentsService.GetStudentsAsync(); + var paginatedResponse = await StudentsService.GetStudentsAsync(); + students = paginatedResponse.Results; totalStudents = students.Count(); } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Connect.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Connect.razor index 32143601..7c376710 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Connect.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Connect.razor @@ -2,7 +2,11 @@ @inject NavigationManager NavigationManager
- + + Connect with Your World @@ -31,3 +35,23 @@ NavigationManager.NavigateTo("/signin"); } } + + diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/Dialogs/DeleteEventDialog.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/Dialogs/DeleteEventDialog.razor new file mode 100644 index 00000000..9d50e22b --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/Dialogs/DeleteEventDialog.razor @@ -0,0 +1,33 @@ +@page "/events/{EventId}/delete" +@using MiniSpace.Web.Areas.Events +@using Radzen +@inject DialogService DialogService +@inject IEventsService EventsService +@inject NavigationManager NavigationManager + + + + + + + + + + + +@code { + [Parameter] + public Guid EventId { get; set; } + + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + } + + private async void DeleteEvent(Guid eventId) + { + await EventsService.DeleteEventAsync(eventId); + DialogService.Close(true); + NavigationManager.NavigateTo("/events/organize"); + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/Dialogs/EventsOrganizeDialog.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/Dialogs/EventsOrganizeDialog.razor index 1f7103eb..00b19b14 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/Dialogs/EventsOrganizeDialog.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/Dialogs/EventsOrganizeDialog.razor @@ -14,7 +14,7 @@ - + @@ -27,15 +27,35 @@ - + - + + + + + +

Not selected parameters:

+
+ +

Sorting parameters (order important):

+
+ +
+
+
+ @@ -57,25 +77,45 @@ private SearchOrganizerEventsModel TempSearchOrganizerEventsModel { get; set; } - private List states = [ "", "Published", - "Cancelled", "Archived" ]; - private List directions = + private List> directions = [ - "Ascending", - "Descending" + new KeyValuePair("Descending", "des"), + new KeyValuePair("Ascending", "asc") ]; + + private List> initialSortParams = + [ + new KeyValuePair("Name", "name"), + new KeyValuePair("Start date", "startDate"), + new KeyValuePair("End date", "endDate"), + ]; + + private IEnumerable> notSelectedSortParams = []; + private IEnumerable> selectedSortParams = []; protected override async Task OnInitializedAsync() { await base.OnInitializedAsync(); TempSearchOrganizerEventsModel = SearchOrganizerEventsModel.DeepClone(); + + List> selectedTmp = new(); + + foreach (var value in TempSearchOrganizerEventsModel.Pageable.Sort.SortBy) + { + var element = initialSortParams.FirstOrDefault(sortParam => sortParam.Value == value); + initialSortParams.Remove(element); + selectedTmp.Add(element); + } + + notSelectedSortParams = initialSortParams; + selectedSortParams = selectedTmp; } private void HandleFiltering() @@ -86,6 +126,8 @@ SearchOrganizerEventsModel.DateFrom = TempSearchOrganizerEventsModel.DateFrom; SearchOrganizerEventsModel.DateTo = TempSearchOrganizerEventsModel.DateTo; SearchOrganizerEventsModel.Pageable = TempSearchOrganizerEventsModel.Pageable.DeepClone(); + SearchOrganizerEventsModel.Pageable.Sort.SortBy = selectedSortParams != null + ? selectedSortParams.Select(sortParam => sortParam.Value) : []; DialogService.Close(true); } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/Dialogs/EventsSearchDialog.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/Dialogs/EventsSearchDialog.razor index 1e81295d..c9b2165c 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/Dialogs/EventsSearchDialog.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/Dialogs/EventsSearchDialog.razor @@ -1,9 +1,13 @@ @page "/events/search/dialog" +@using MiniSpace.Web.DTO @using MiniSpace.Web.Models.Events +@using MiniSpace.Web.Models.Organizations @using Radzen @using Blazorise.DeepCloner +@using MiniSpace.Web.Areas.Organizations @inject NavigationManager NavigationManager @inject Radzen.DialogService DialogService +@inject IOrganizationsService OrganizationsService @@ -17,7 +21,7 @@ - + @@ -33,15 +37,61 @@ - + - + - + + + + + +

Not selected parameters:

+
+ +

Sorting parameters (order important):

+
+ +
+
+
+ + + + @if (totalRootOrganizations == 0) + { +

There are not any organizations created.

+ } + +

Select an organization (also suborganizations are included in results):

+ + + + + + + false)/> + +
+
+ + + + + + + @@ -82,31 +132,87 @@ [ "", "Published", - "Cancelled", "Archived" ]; - private List directions = + private List> directions = [ - "Ascending", - "Descending" + new KeyValuePair("Descending", "des"), + new KeyValuePair("Ascending", "asc") ]; + + private List> initialSortParams = + [ + new KeyValuePair("Name", "name"), + new KeyValuePair("Capacity", "capacity"), + new KeyValuePair("Start date", "startDate"), + new KeyValuePair("End date", "endDate"), + ]; + + private IEnumerable> notSelectedSortParams = []; + private IEnumerable> selectedSortParams = []; + + private int totalRootOrganizations = 0; + private List rootOrganizations = new(); + private object selectedItem; protected override async Task OnInitializedAsync() { await base.OnInitializedAsync(); TempSearchEventsModel = SearchEventsModel.DeepClone(); + + List> selectedTmp = new(); + + foreach (var value in TempSearchEventsModel.Pageable.Sort.SortBy) + { + var element = initialSortParams.FirstOrDefault(sortParam => sortParam.Value == value); + initialSortParams.Remove(element); + selectedTmp.Add(element); + } + + notSelectedSortParams = initialSortParams; + selectedSortParams = selectedTmp; + + var tmpOrganizations = await OrganizationsService.GetRootOrganizationsAsync(); + ConvertOrganizationDtoList(tmpOrganizations, rootOrganizations, null); + totalRootOrganizations = rootOrganizations.Count; + } + + private async void OnExpand(TreeExpandEventArgs args) + { + var parent = (OrganizationModel)args.Value; + var childOrganizations = await OrganizationsService.GetChildrenOrganizationsAsync(parent.Id, parent.RootId); + ConvertOrganizationDtoList(childOrganizations, parent.Children, parent.RootId); + StateHasChanged(); + } + + private static void ConvertOrganizationDtoList(IEnumerable input, IList result, Guid? rootId) + { + result.Clear(); + foreach (var organization in input) + { + result.Add(new OrganizationModel() + { + Id = organization.Id, + Name = organization.Name, + RootId = rootId ?? organization.Id, + Children = new List() + }); + } } private void HandleFiltering() { SearchEventsModel.Name = TempSearchEventsModel.Name; SearchEventsModel.Organizer = TempSearchEventsModel.Organizer; + SearchEventsModel.Organization = selectedItem != null ? (OrganizationModel)selectedItem: new OrganizationModel(); SearchEventsModel.Category = TempSearchEventsModel.Category; SearchEventsModel.State = TempSearchEventsModel.State; SearchEventsModel.DateFrom = TempSearchEventsModel.DateFrom; SearchEventsModel.DateTo = TempSearchEventsModel.DateTo; SearchEventsModel.Pageable = TempSearchEventsModel.Pageable.DeepClone(); + SearchEventsModel.Pageable.Sort.SortBy = selectedSortParams != null + ? selectedSortParams.Select(sortParam => sortParam.Value) : []; DialogService.Close(true); } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/Dialogs/ParticipantDetailsDialog.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/Dialogs/ParticipantDetailsDialog.razor new file mode 100644 index 00000000..53f576ed --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/Dialogs/ParticipantDetailsDialog.razor @@ -0,0 +1,150 @@ +@page "/events/{EventId}/participants/{ParticipantDto.Id}/details" +@using MiniSpace.Web.Areas.Students +@using Radzen +@using MiniSpace.Web.DTO +@inject DialogService DialogService +@inject IIdentityService IdentityService +@inject IStudentsService StudentsService + +@if (dialogInitialized) +{ + + + + + + Identification + + + + + + + + + + + + + + + + + + + + + + + + + + Personal info + + + + + + + First name + @(studentDto.FirstName) + + + + Last name + @(studentDto.LastName) + + + + + + Email + @(studentDto.Email) + + + + Description + @(studentDto.Description) + + + + + + + + + + + + + + + + + Other info + + + + + + + Status + @(studentDto.State.Normalize()) + + + + Email notifications + @(studentDto.EmailNotifications) + + + + + + Date of birth + @(studentDto.DateOfBirth.ToLocalTime().ToString(shortDateFormat)) + + + + Created at + @(studentDto.CreatedAt.ToLocalTime().ToString(dateFormat)) + + + + + + + + + + + + + + + + + + +} + +@code { + [Parameter] + public string EventId { get; set; } + [Parameter] + public ParticipantDto ParticipantDto { get; set; } + + private StudentDto studentDto = new(); + private bool dialogInitialized = false; + + private const string dateFormat = "dd/MM/yyyy HH:mm"; + private const string shortDateFormat = "dd/MM/yyyy"; + + protected override async Task OnInitializedAsync() + { + studentDto = await StudentsService.GetStudentAsync(ParticipantDto.StudentId); + await base.OnInitializedAsync(); + dialogInitialized = true; + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/Event.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/Event.razor index 5eafb9ae..10b3485f 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/Event.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/Event.razor @@ -3,6 +3,8 @@ @using MiniSpace.Web.Areas.Events @using MiniSpace.Web.Pages.Events.Dialogs @using MiniSpace.Web.Areas.Posts +@using MiniSpace.Web.Areas.Reactions +@using MiniSpace.Web.DTO.Enums @using Radzen @using AlignItems = Radzen.AlignItems @using DialogOptions = Radzen.DialogOptions @@ -12,6 +14,7 @@ @inject IIdentityService IdentityService @inject IEventsService EventsService @inject IPostsService PostsService +@inject IReactionsService ReactionsService @inject NavigationManager NavigationManager @if (!pageInitialized) @@ -97,7 +100,8 @@ - + @@ -105,9 +109,28 @@ Click="@(() => NavigationManager.NavigateTo($"/events/{ev.Id}/posts/create"))" /> } + + + + + + Number of reactions + @(reactionsSummary.NumberOfReactions) + + + + + Dominant reaction + @(ReactionTypeExtensions.GetReactionText(reactionsSummary.DominantReaction)) + + + + - + @if (pageInitialized && !posts.Any()) @@ -156,10 +179,113 @@ + @if (pageInitialized && !reactions.Any()) + { +

No reactions have been added by students yet.

+ } + + +
- - + @if (IdentityService.IsAuthenticated && !IsUserEventOrganizer(ev)) + { + + + + + + + + + + + + + + + + + + + + + + } + + @if (IdentityService.IsAuthenticated && IsUserEventOrganizer(ev)) + { + + + + + + + + + + + + + + + + + + + + + + }
@@ -176,6 +302,16 @@ private bool pageInitialized = false; IEnumerable posts; + IEnumerable reactions; + + ReactionsSummaryDto reactionsSummary = new(); + List> reactionTypes = ReactionTypeExtensions.GenerateReactionPairs(); + + private int participantPageSize = 10; + IEnumerable participantPageSizeOptions = new int[] { 10, 20, 40}; + RadzenDataGrid signedUpDataGrid; + List signedUpStudents; + List interestedStudents; protected override async Task OnInitializedAsync() { @@ -184,7 +320,9 @@ studentId = IdentityService.GetCurrentUserId(); } ev = await EventsService.GetEventAsync(new Guid(EventId)); - posts = await PostsService.GetPostsAsync(new Guid(EventId)); + reactionsSummary = await ReactionsService.GetReactionsSummary(ev.Id, ReactionContentType.Event); + posts = await PostsService.GetPostsAsync(ev.Id); + reactions = await ReactionsService.GetReactions(ev.Id, ReactionContentType.Event); pageInitialized = true; } @@ -211,14 +349,30 @@ return; } - pageInitialized = false; switch (index) { case 0: posts = await PostsService.GetPostsAsync(ev.Id); break; + case 2: + reactions = await ReactionsService.GetReactions(ev.Id, ReactionContentType.Event); + break; + case 3: + if (IsUserEventOrganizer(ev)) + { + var eventParticipantsDto = await EventsService.GetEventParticipants(ev.Id); + signedUpStudents = eventParticipantsDto.SignedUpStudents.ToList(); + } + break; + case 4: + if (IsUserEventOrganizer(ev)) + { + var eventParticipantsDto = await EventsService.GetEventParticipants(ev.Id); + interestedStudents = eventParticipantsDto.InterestedStudents.ToList(); + } + break; } - pageInitialized = true; + StateHasChanged(); } private async Task OpenEventDetailsDialog(EventDto eventDto) @@ -232,6 +386,32 @@ }); } + private async Task OpenDeleteEventDialog(Guid postId) + { + await DialogService.OpenAsync("Are you sure? This action cannot be undone!", + new Dictionary() { {"EventId", new Guid(EventId) } }, + new DialogOptions() + { + Width = "500px", Height = "100px", Resizable = true, Draggable = true, + AutoFocusFirstElement = false + }); + } + + private async Task OpenParticipantDetailsDialog(ParticipantDto participantDto, string term) + { + await DialogService.OpenAsync($"Details of the {term}:", + new Dictionary() + { + { "EventId", EventId }, + { "ParticipantDto", participantDto } + }, + new DialogOptions() + { + Width = "700px", Height = "600px", Resizable = true, Draggable = true, + AutoFocusFirstElement = false + }); + } + private async void SignUpToEvent(EventDto eventDto) { await EventsService.SignUpToEventAsync(eventDto.Id, studentId); @@ -259,4 +439,28 @@ eventDto.IsInterested = false; StateHasChanged(); } + + private async void RemoveEventParticipant(EventDto eventDto, ParticipantDto participant) + { + await EventsService.RemoveEventParticipant(eventDto.Id, participant.StudentId); + signedUpStudents.Remove(participant); + await signedUpDataGrid.Reload(); + } + + private async void ReactionChanged(ReactionType? reactionType) + { + if (reactionsSummary.AuthUserReactionId != null) + { + await ReactionsService.DeleteReaction((Guid)reactionsSummary.AuthUserReactionId); + } + + if (reactionType != null) + { + await ReactionsService.CreateReaction(Guid.Empty, studentId, reactionType.ToString(), + ev.Id, ReactionContentType.Event.ToString()); + } + + reactionsSummary = await ReactionsService.GetReactionsSummary(ev.Id, ReactionContentType.Event); + StateHasChanged(); + } } \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/EventAdd.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/EventCreate.razor similarity index 54% rename from MiniSpace.Web/src/MiniSpace.Web/Pages/Events/EventAdd.razor rename to MiniSpace.Web/src/MiniSpace.Web/Pages/Events/EventCreate.razor index da81391a..288af1f8 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/EventAdd.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/EventCreate.razor @@ -1,103 +1,113 @@ -@page "/events/add" +@page "/events/create" @using MiniSpace.Web.Areas.Identity @using MiniSpace.Web.Areas.Events @using MiniSpace.Web.Areas.Http +@using MiniSpace.Web.Areas.MediaFiles @using MiniSpace.Web.Areas.Organizations @using MiniSpace.Web.DTO +@using MiniSpace.Web.DTO.Types @using MiniSpace.Web.Models.Events @using Radzen +@using System.IO @inject IIdentityService IdentityService @inject IEventsService EventsService @inject IOrganizationsService OrganizationsService +@inject IMediaFilesService MediaFilesService @inject IErrorMapperService ErrorMapperService @inject NavigationManager NavigationManager -

Add new event

+

Create new event

@if (!pageInitialized) {

Loading...

} -@if (pageInitialized && organizerId != Guid.Empty) +@if (pageInitialized && !organizationsFound) +{ +

You have been granted the organizer rights, but have not been added to any organization yet!

+

Therefore, you cannot create any event.

+} + +@if (pageInitialized && organizationsFound && organizerId != Guid.Empty) { @errorMessage - + - + - - + - + - + - + - + - - - - - - - + + + + + + + - + - + - + - + @@ -112,10 +122,10 @@ @if (publishInfo == 2) { - } @@ -129,7 +139,7 @@ - + - + + + + + + + @if (isUploading) + { + + + + } + + + Choose files to upload (max 5) + + + + + + @@ -152,8 +184,12 @@ @code { private Guid organizerId; private bool pageInitialized = false; + private bool organizationsFound = false; + private TaskCompletionSource clientChangeCompletionSource; + private bool isUploading = false; + private Dictionary images = new (); - private AddEventModel addEventModel = new() + private CreateEventModel _createEventModel = new() { Name = "One of first events!", Category = "Art", @@ -200,9 +236,14 @@ { organizerId = IdentityService.GetCurrentUserId(); organizations = await OrganizationsService.GetOrganizerOrganizationsAsync(organizerId); - - addEventModel.OrganizerId = organizerId; - addEventModel.OrganizationId = organizations.First().Id; + + if (organizations.Any()) + { + organizationsFound = true; + _createEventModel.EventId = Guid.NewGuid(); + _createEventModel.OrganizerId = organizerId; + _createEventModel.Organization = organizations.First(); + } } pageInitialized = true; @@ -210,14 +251,14 @@ private async Task HandleCreateEvent() { - var response = await EventsService.AddEventAsync(Guid.Empty, addEventModel.Name, - addEventModel.OrganizerId, addEventModel.OrganizationId, - addEventModel.StartDate.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), - addEventModel.EndDate.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), - addEventModel.BuildingName, addEventModel.Street, addEventModel.BuildingNumber, - addEventModel.ApartmentNumber, addEventModel.City, addEventModel.ZipCode, - addEventModel.Description, addEventModel.Capacity, addEventModel.Fee, addEventModel.Category, - publishInfo == 2 ? addEventModel.PublishDate.ToString("yyyy-MM-ddTHH:mm:ss.fffZ") : string.Empty); + var response = await EventsService.CreateEventAsync(_createEventModel.EventId, _createEventModel.Name, + _createEventModel.OrganizerId, _createEventModel.Organization.Id, _createEventModel.Organization.RootId, + _createEventModel.StartDate.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), + _createEventModel.EndDate.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), + _createEventModel.BuildingName, _createEventModel.Street, _createEventModel.BuildingNumber, + _createEventModel.ApartmentNumber, _createEventModel.City, _createEventModel.ZipCode, images.Select(o => o.Value), + _createEventModel.Description, _createEventModel.Capacity, _createEventModel.Fee, _createEventModel.Category, + publishInfo == 2 ? _createEventModel.PublishDate.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ") : string.Empty); if (response.ErrorMessage != null) { @@ -229,4 +270,62 @@ NavigationManager.NavigateTo("/events/organize"); } } + + async void OnClientChange(UploadChangeEventArgs args) + { + @* Console.WriteLine("Client-side upload changed"); *@ + clientChangeCompletionSource = new TaskCompletionSource(); + var uploadedImages = new Dictionary(); + isUploading = true; + + foreach (var file in args.Files) + { + StateHasChanged(); + if (images.TryGetValue(file.Name, out var imageId)) + { + uploadedImages.Add(file.Name, imageId); + continue; + } + + try + { + long maxFileSize = 10 * 1024 * 1024; + var stream = file.OpenReadStream(maxFileSize); + byte[] bytes = await ReadFully(stream); + var base64Content = Convert.ToBase64String(bytes); + var response = await MediaFilesService.UploadMediaFileAsync(_createEventModel.EventId, + MediaFileContextType.Event.ToString(), IdentityService.UserDto.Id, + file.Name, file.ContentType, base64Content); + if (response.Content != null && response.Content.FileId != Guid.Empty) + { + uploadedImages.Add(file.Name, response.Content.FileId); + } + stream.Close(); + } + catch (Exception ex) + { + @* Console.WriteLine($"Client-side file read error: {ex.Message}"); *@ + } + finally + { + + } + } + isUploading = false; + StateHasChanged(); + images = uploadedImages; + clientChangeCompletionSource.SetResult(true); + } + + private static async Task ReadFully(Stream input) + { + byte[] buffer = new byte[16*1024]; + using MemoryStream ms = new MemoryStream(); + int read; + while ((read = await input.ReadAsync(buffer, 0, buffer.Length)) > 0) + { + ms.Write(buffer, 0, read); + } + return ms.ToArray(); + } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/EventUpdate.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/EventUpdate.razor index 85ac9b69..5d97365d 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/EventUpdate.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/EventUpdate.razor @@ -38,7 +38,7 @@ + ShowTime="true" MinutesStep="5" DateFormat="@dateFormat" /> @@ -72,7 +72,7 @@ + ShowTime="true" MinutesStep="5" DateFormat="@dateFormat" /> @@ -107,7 +107,7 @@ { + ShowTime="true" MinutesStep="5" DateFormat="@dateFormat" /> @@ -147,6 +147,8 @@ [Parameter] public string EventId { get; set; } + private const string dateFormat = "dd/MM/yyyy HH:mm"; + private Guid organizerId; private EventDto eventDto; private bool pageInitialized = false; @@ -185,14 +187,15 @@ updateEventModel.EventId = eventDto.Id; updateEventModel.Name = eventDto.Name; updateEventModel.OrganizerId = eventDto.Organizer.Id; - updateEventModel.StartDate = eventDto.StartDate; - updateEventModel.EndDate = eventDto.EndDate; + updateEventModel.StartDate = eventDto.StartDate.ToLocalTime(); + updateEventModel.EndDate = eventDto.EndDate.ToLocalTime(); updateEventModel.BuildingName = eventDto.Location.BuildingName; updateEventModel.Street = eventDto.Location.Street; updateEventModel.BuildingNumber = eventDto.Location.BuildingNumber; updateEventModel.ApartmentNumber = eventDto.Location.ApartmentNumber; updateEventModel.City = eventDto.Location.City; updateEventModel.ZipCode = eventDto.Location.ZipCode; + updateEventModel.MediaFiles = eventDto.MediaFiles; updateEventModel.Description = eventDto.Description; updateEventModel.Capacity = eventDto.Capacity; updateEventModel.Fee = eventDto.Fee; @@ -206,12 +209,12 @@ { var response = await EventsService.UpdateEventAsync(updateEventModel.EventId, updateEventModel.Name, updateEventModel.OrganizerId, - updateEventModel.StartDate.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), - updateEventModel.EndDate.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), + updateEventModel.StartDate.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), + updateEventModel.EndDate.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), updateEventModel.BuildingName, updateEventModel.Street, updateEventModel.BuildingNumber, - updateEventModel.ApartmentNumber, updateEventModel.City, updateEventModel.ZipCode, + updateEventModel.ApartmentNumber, updateEventModel.City, updateEventModel.ZipCode, updateEventModel.MediaFiles, updateEventModel.Description, updateEventModel.Capacity, updateEventModel.Fee, updateEventModel.Category, - publishInfo == 2 ? updateEventModel.PublishDate.ToString("yyyy-MM-ddTHH:mm:ss.fffZ") : string.Empty); + publishInfo == 2 ? updateEventModel.PublishDate.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ") : string.Empty); if (response.ErrorMessage != null) { diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/Events.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/Events.razor deleted file mode 100644 index c728a2e1..00000000 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/Events.razor +++ /dev/null @@ -1,141 +0,0 @@ -@page "/events" -@using MiniSpace.Web.Areas.Students -@using MiniSpace.Web.DTO -@using MiniSpace.Web.Areas.Events -@using MiniSpace.Web.Pages.Events.Dialogs -@using Radzen -@using DialogOptions = Radzen.DialogOptions -@using DialogService = Radzen.DialogService -@inject DialogService DialogService -@inject IIdentityService IdentityService -@inject IEventsService EventsService -@inject NavigationManager NavigationManager - -

Follow events

- -@if (!pageInitialized) -{ -

Loading...

-} - -@if (pageInitialized && studentId != Guid.Empty) -{ - - - - - @if (totalSignedUpElements == 0) - { -

You haven't been signed up for any event yet.

- } - - - -
- - - @if (totalInterestedElements == 0) - { -

You haven't been interested in any event yet.

- } - - - -
-
-
-
-} - -@code { - private const string dateFormat = "dd/MM/yyyy HH:mm"; - - private Guid studentId; - private bool pageInitialized = false; - - private int numberOfResults = 10; - - int totalSignedUpElements = 0; - IEnumerable signedUpEvents; - - int totalInterestedElements = 0; - IEnumerable interestedEvents; - - protected override async Task OnInitializedAsync() - { - if (IdentityService.IsAuthenticated) - { - studentId = IdentityService.GetCurrentUserId(); - - var tmp = await EventsService.GetStudentEventsAsync(studentId, numberOfResults); - if (tmp != null) - { - signedUpEvents = tmp.Content.Where(ev => ev.IsSignedUp); - totalSignedUpElements = signedUpEvents.Count(); - - interestedEvents = tmp.Content.Where(ev => ev.IsInterested); - totalInterestedElements = interestedEvents.Count(); - } - else - { - signedUpEvents = new List(); - totalSignedUpElements = 0; - - interestedEvents = new List(); - totalInterestedElements = 0; - } - } - - pageInitialized = true; - } -} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/EventsFollow.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/EventsFollow.razor new file mode 100644 index 00000000..05de994b --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/EventsFollow.razor @@ -0,0 +1,192 @@ +@page "/events/follow" +@using MiniSpace.Web.Areas.Students +@using MiniSpace.Web.DTO +@using MiniSpace.Web.Areas.Events +@using MiniSpace.Web.Components +@using MiniSpace.Web.Pages.Events.Dialogs +@using MudBlazor +@using Radzen +@using DialogOptions = Radzen.DialogOptions +@using DialogService = Radzen.DialogService +@using Orientation = Radzen.Orientation +@inject DialogService DialogService +@inject IIdentityService IdentityService +@inject IEventsService EventsService +@inject NavigationManager NavigationManager + +

Follow events

+ +@if (!pageInitialized) +{ +

Loading...

+} + +@if (pageInitialized && studentId != Guid.Empty) +{ + + + + + @if (signedUpTotalElements == 0) + { +

You haven't been signed up for any event yet.

+ } + else + { + + + + + + + + + + + + } +
+ + + @if (interestedInTotalElements == 0) + { +

You haven't been interested in any event yet.

+ } + else + { + + + + + + + + + + + + } +
+
+
+
+} + +@code { + private Guid studentId; + private bool pageInitialized = false; + + int signedUpPageNumber = 1; + int signedUpPageSize = 5; + int signedUpTotalPages = 0; + int signedUpTotalElements = 0; + IEnumerable signedUpEvents; + + int interestedInPageNumber = 1; + int interestedInPageSize = 5; + int interestedInTotalPages = 0; + int interestedInTotalElements = 0; + IEnumerable interestedInEvents; + + protected override async Task OnInitializedAsync() + { + if (IdentityService.IsAuthenticated) + { + studentId = IdentityService.GetCurrentUserId(); + + var tmp = await EventsService.GetStudentEventsAsync(studentId, "SignedUp", + signedUpPageNumber, signedUpPageSize); + if (tmp != null) + { + signedUpTotalPages = tmp.TotalPages; + signedUpTotalElements = tmp.TotalElements; + signedUpEvents = tmp.Content; + } + else + { + signedUpTotalPages = 0; + signedUpTotalElements = 0; + signedUpEvents = new List(); + } + } + + pageInitialized = true; + } + + private async void OnChange(int index) + { + if (!IdentityService.IsAuthenticated) + { + return; + } + + switch (index) + { + case 0: + var tmp = await EventsService.GetStudentEventsAsync(studentId, "SignedUp", + signedUpPageNumber, signedUpPageSize); + if (tmp != null) + { + signedUpTotalPages = tmp.TotalPages; + signedUpTotalElements = tmp.TotalElements; + signedUpEvents = tmp.Content; + } + else + { + signedUpTotalPages = 0; + signedUpTotalElements = 0; + signedUpEvents = new List(); + } + break; + case 1: + tmp = await EventsService.GetStudentEventsAsync(studentId, "InterestedIn", + interestedInPageNumber, interestedInPageSize); + if (tmp != null) + { + interestedInTotalPages = tmp.TotalPages; + interestedInTotalElements = tmp.TotalElements; + interestedInEvents = tmp.Content; + } + else + { + interestedInTotalPages = 0; + interestedInTotalElements = 0; + interestedInEvents = new List(); + } + break; + } + StateHasChanged(); + } + + private async void SignedUpSelectedPageChanged(int pageNumber) + { + signedUpPageNumber = pageNumber; + + var tmp = await EventsService.GetStudentEventsAsync(studentId, "SignedUp", + signedUpPageNumber, signedUpPageSize); + signedUpEvents = tmp.Content; + StateHasChanged(); + } + + private async void InterestedInSelectedPageChanged(int pageNumber) + { + interestedInPageNumber = pageNumber; + + var tmp = await EventsService.GetStudentEventsAsync(studentId, "InterestedIn", + interestedInPageNumber, interestedInPageSize); + interestedInEvents = tmp.Content; + StateHasChanged(); + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/EventsOrganize.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/EventsOrganize.razor index 8e5ab86d..f9623a0d 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/EventsOrganize.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/EventsOrganize.razor @@ -1,6 +1,7 @@ @page "/events/organize" @using MiniSpace.Web.Areas.Events @using MiniSpace.Web.Areas.Students +@using MiniSpace.Web.Components @using MiniSpace.Web.DTO @using MiniSpace.Web.DTO.Wrappers @using MiniSpace.Web.Models.Events @@ -20,8 +21,8 @@
- + @@ -48,30 +49,7 @@ @@ -85,13 +63,11 @@ } @code { - private const string dateFormat = "dd/MM/yyyy HH:mm"; - private SearchOrganizerEventsModel searchOrganizerEventsModel = new() { Name = "", State = "", - DateFrom = new DateTime(2024, 04, 14), + DateFrom = new DateTime(2024, 05, 14), DateTo = new DateTime(2024, 05, 31), Pageable = new PageableDto() { @@ -99,8 +75,8 @@ Size = 5, Sort = new SortDto() { - SortBy = new List() { "dateFrom" }, - Direction = "Ascending" + SortBy = new List() { "name" }, + Direction = "des" } } }; @@ -121,8 +97,8 @@ var tmp = await EventsService.SearchOrganizerEventsAsync(searchOrganizerEventsModel.OrganizerId, searchOrganizerEventsModel.Name, searchOrganizerEventsModel.State, - searchOrganizerEventsModel.DateFrom.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), - searchOrganizerEventsModel.DateTo.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), + searchOrganizerEventsModel.DateFrom.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), + searchOrganizerEventsModel.DateTo.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), searchOrganizerEventsModel.Pageable); if (tmp.Content != null) { @@ -147,8 +123,8 @@ var tmp = await EventsService.SearchOrganizerEventsAsync(searchOrganizerEventsModel.OrganizerId, searchOrganizerEventsModel.Name, searchOrganizerEventsModel.State, - searchOrganizerEventsModel.DateFrom.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), - searchOrganizerEventsModel.DateTo.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), + searchOrganizerEventsModel.DateFrom.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), + searchOrganizerEventsModel.DateTo.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), searchOrganizerEventsModel.Pageable); events = tmp.Content.Content; StateHasChanged(); @@ -160,7 +136,7 @@ new Dictionary() { { "SearchOrganizerEventsModel", searchOrganizerEventsModel } }, new DialogOptions() { - Width = "700px", Resizable = true, Draggable = true, + Width = "800px", Resizable = true, Draggable = true, AutoFocusFirstElement = false }); await OnInitializedAsync(); diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/EventsSearch.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/EventsSearch.razor index b8e45309..5b2a59d6 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/EventsSearch.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/EventsSearch.razor @@ -2,9 +2,11 @@ @using MiniSpace.Web.Areas.Identity @using MiniSpace.Web.Areas.Events @using MiniSpace.Web.Areas.Students +@using MiniSpace.Web.Components @using MiniSpace.Web.DTO @using MiniSpace.Web.DTO.Wrappers @using MiniSpace.Web.Models.Events +@using MiniSpace.Web.Models.Organizations @using MiniSpace.Web.Pages.Events.Dialogs @using MudBlazor @using Radzen @@ -35,60 +37,38 @@ @if (pageInitialized && totalElements != 0) { - } @if (pageInitialized && totalElements != 0) { - } @code { - private const string dateFormat = "dd/MM/yyyy HH:mm"; - private SearchEventsModel searchEventsModel = new() { Name = "", Organizer = "", + Organization = new OrganizationModel(), Category = "", State = "", Friends = [], FriendsEngagementType = "", - DateFrom = new DateTime(2024, 04, 14), + DateFrom = new DateTime(2024, 05, 14), DateTo = new DateTime(2024, 05, 31), Pageable = new PageableDto() { @@ -96,8 +76,8 @@ Size = 5, Sort = new SortDto() { - SortBy = new List() { "dateFrom" }, - Direction = "Ascending" + SortBy = new List() { "name" }, + Direction = "des" } } }; @@ -110,11 +90,11 @@ protected override async Task OnInitializedAsync() { - var tmp = await EventsService.SearchEventsAsync(searchEventsModel.Name, - searchEventsModel.Organizer, searchEventsModel.Category, searchEventsModel.State, - searchEventsModel.Friends, searchEventsModel.FriendsEngagementType, - searchEventsModel.DateFrom.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), - searchEventsModel.DateTo.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), + var tmp = await EventsService.SearchEventsAsync(searchEventsModel.Name, searchEventsModel.Organizer, + searchEventsModel.Organization.Id, searchEventsModel.Organization.RootId, searchEventsModel.Category, + searchEventsModel.State, searchEventsModel.Friends, searchEventsModel.FriendsEngagementType, + searchEventsModel.DateFrom.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), + searchEventsModel.DateTo.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), searchEventsModel.Pageable); if (tmp.Content != null) { @@ -136,11 +116,11 @@ { searchEventsModel.Pageable.Page = pageNumber; - var tmp = await EventsService.SearchEventsAsync(searchEventsModel.Name, - searchEventsModel.Organizer, searchEventsModel.Category, searchEventsModel.State, - searchEventsModel.Friends, searchEventsModel.FriendsEngagementType, - searchEventsModel.DateFrom.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), - searchEventsModel.DateTo.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), + var tmp = await EventsService.SearchEventsAsync(searchEventsModel.Name, searchEventsModel.Organizer, + searchEventsModel.Organization.Id, searchEventsModel.Organization.RootId, searchEventsModel.Category, + searchEventsModel.State, searchEventsModel.Friends, searchEventsModel.FriendsEngagementType, + searchEventsModel.DateFrom.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), + searchEventsModel.DateTo.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), searchEventsModel.Pageable); events = tmp.Content.Content; StateHasChanged(); @@ -152,7 +132,7 @@ new Dictionary() { { "SearchEventsModel", searchEventsModel } }, new DialogOptions() { - Width = "700px", Resizable = true, Draggable = true, + Width = "800px", Height = "650px", Resizable = true, Draggable = true, AutoFocusFirstElement = false }); await OnInitializedAsync(); diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/Friends.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/Friends.razor index 0eabfbcd..b3318f97 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/Friends.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/Friends.razor @@ -8,6 +8,22 @@ @inject NavigationManager NavigationManager @inject Radzen.NotificationService NotificationService @inject Radzen.DialogService DialogService +@using MiniSpace.Web.Areas.MediaFiles +@inject IMediaFilesService MediaFilesService +@using MudBlazor + + + + +@code { + private List _items = new List + { + new BreadcrumbItem("Home", href: "/", icon: Icons.Material.Filled.Home), + new BreadcrumbItem("Friends", href: "/friends", icon: Icons.Material.Filled.VideoLibrary), + }; +}
+ @code { + private string searchTerm; + private List students = new List(); private IEnumerable incomingRequests; + private IEnumerable filteredIncomingRequests; + private Dictionary images = new(); protected override async Task OnInitializedAsync() { incomingRequests = await FriendsService.GetIncomingFriendRequestsAsync(); + + if (incomingRequests == null || !incomingRequests.Any()) + { + @* Console.WriteLine("No incoming friend requests found."); *@ + return; + } + + var inviterIds = incomingRequests.Select(r => r.InviterId).Distinct(); + var studentTasks = inviterIds.Select(id => FriendsService.GetStudentAsync(id)); + + students = (await Task.WhenAll(studentTasks)).ToList(); + + var imageTasks = students.Select(student => FetchImageAsync(student.Id, student.ProfileImage)); + await Task.WhenAll(imageTasks); + + foreach (var request in incomingRequests) + { + var student = students.FirstOrDefault(s => s.Id == request.InviterId); + if (student != null) + { + request.InviterName = $"{student.FirstName} {student.LastName}"; + request.InviterEmail = student.Email; + if (images.ContainsKey(student.Id)) + { + request.InviterImage = images[student.Id]; + } + } + } + + filteredIncomingRequests = incomingRequests; + } + + private async Task FetchImageAsync(Guid inviterId, Guid profileImage) + { + var result = await MediaFilesService.GetFileAsync(profileImage); + images[inviterId] = result.Base64Content; + } + + private string GetImage(string base64Image) + { + if (string.IsNullOrWhiteSpace(base64Image)) + { + return "images/user_image.png"; + } + else + { + return $"data:image/jpeg;base64,{base64Image}"; + } + } + + private void SearchIncomingRequests() + { + filteredIncomingRequests = string.IsNullOrWhiteSpace(searchTerm) + ? incomingRequests + : incomingRequests.Where(x => x.InviterName.Contains(searchTerm, StringComparison.OrdinalIgnoreCase)); + } + + private void RedirectToDetails(Guid id) + { + NavigationManager.NavigateTo($"/student-details/{id}"); } private async Task AcceptRequest(Guid requestId) @@ -51,9 +264,12 @@ else if (request != null) { await FriendsService.AcceptFriendRequestAsync(request.Id, request.InviterId, request.InviteeId); + await JSRuntime.InvokeVoidAsync("playNotificationSound"); + NotificationService.Notify(Radzen.NotificationSeverity.Success, "Friend Request Accepted", duration: 4000); + StateHasChanged(); incomingRequests = incomingRequests.Where(r => r.Id != requestId).ToList(); + SearchIncomingRequests(); // Reapply search filter to update the UI NotificationService.Notify(Radzen.NotificationSeverity.Success, "Request Accepted", duration: 4000); - StateHasChanged(); // Refresh the UI } } @@ -64,9 +280,8 @@ else { await FriendsService.DeclineFriendRequestAsync(request.Id, request.InviterId, request.InviteeId); incomingRequests = incomingRequests.Where(r => r.Id != requestId).ToList(); + SearchIncomingRequests(); // Reapply search filter to update the UI NotificationService.Notify(Radzen.NotificationSeverity.Warning, "Request Declined", duration: 4000); - StateHasChanged(); // Update the UI } } - } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/FriendsSearch.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/FriendsSearch.razor index aa401476..0b6859df 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/FriendsSearch.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/FriendsSearch.razor @@ -7,10 +7,24 @@ @inject IFriendsService FriendsService @using MiniSpace.Web.DTO @using MiniSpace.Web.Areas.Identity +@using MiniSpace.Web.Areas.MediaFiles @inject IIdentityService IdentityService +@inject IMediaFilesService MediaFilesService @inject Radzen.NotificationService NotificationService @inject IJSRuntime JSRuntime +@using MudBlazor + +@code { + private List _items = new List + { + new BreadcrumbItem("Home", href: "/", icon: Icons.Material.Filled.Home), + new BreadcrumbItem("Friends", href: "/friends", icon: Icons.Material.Filled.VideoLibrary), + new BreadcrumbItem("Search", href: "/friends/search", disabled: true, icon: Icons.Material.Filled.Create) + }; +}
@@ -22,7 +36,10 @@ @foreach (var student in students) {
- Student Image + @if(images.ContainsKey(student.Id)) + { + Student Image + }
@student.FirstName @student.LastName

Email: @student.Email

@@ -31,17 +48,34 @@ Click="@(e => ConnectWithStudent(student.Id, e))" ButtonStyle="Radzen.ButtonStyle.Secondary" Style="width: 100%; margin-top: 8px;" /> *@ - - @if (!sentRequests.Any(r => r.InviteeId == student.Id)) + + @if (student.Id != IdentityService.GetCurrentUserId() && !sentRequests.Any(r => r.InviteeId == student.Id) && !allFriends.Any(f => f.FriendId == student.Id)) { + ButtonStyle="Radzen.ButtonStyle.Secondary" + Style="width: 100%; margin-top: 8px; border-radius: 5px;" /> } - else + else if (allFriends.Any(f => f.FriendId == student.Id)) + { + + } + else if (sentRequests.Any(r => r.InviteeId == student.Id)) { + Style="width: 100%; margin-top: 8px; background-color: #ccc; color: black; border-radius: 5px;" /> + } + else if (incomingRequests.Any(r => r.InviteeId == IdentityService.GetCurrentUserId() && r.State == DTO.States.FriendState.Requested)) + { + + } + else if (student.Id == IdentityService.GetCurrentUserId()) + { + }
@@ -60,7 +94,7 @@
- Profile Image + Profile Image

@student?.FirstName @student?.LastName

    @@ -68,21 +102,40 @@
  • Email: @student?.Email
  • Description: @student?.Description
  • Number of Friends: @student?.NumberOfFriends
  • -
  • Date of Birth: @student?.DateOfBirth.ToString("yyyy-MM-dd")
  • +
  • Date of Birth: @student?.DateOfBirth.ToLocalTime().ToString("yyyy-MM-dd")
  • State: @student?.State
  • -
  • Created At: @student?.CreatedAt.ToString("yyyy-MM-dd")
  • +
  • Created At: @student?.CreatedAt.ToLocalTime().ToString("yyyy-MM-dd")
- @if (!sentRequests.Any(r => r.InviteeId == student.Id)) +
+ + @if (student.Id != IdentityService.GetCurrentUserId() && !sentRequests.Any(r => r.InviteeId == student.Id) && !allFriends.Any(f => f.FriendId == student.Id)) { + ButtonStyle="Radzen.ButtonStyle.Secondary" + Style="width: 100%; margin-top: 8px; border-radius: 5px;" /> + } + else if (allFriends.Any(f => f.FriendId == student.Id)) + { + } - else + else if (sentRequests.Any(r => r.InviteeId == student.Id)) { + Style="width: 100%; margin-top: 8px; background-color: #ccc; color: black; border-radius: 5px;" /> + } + else if (incomingRequests.Any(r => r.InviteeId == IdentityService.GetCurrentUserId() && r.State == DTO.States.FriendState.Requested)) + { + + } + else if (student.Id == IdentityService.GetCurrentUserId()) + { + } + +
@@ -92,7 +145,6 @@ {

Select a student to view details.

- using MiniSpace.Services.Friends.Application.Dto;
}
@@ -137,12 +189,12 @@ } .left-panel, .right-panel { - background-color: #f7f7f7; /* Soft background color for subtle contrast */ - border-radius: 8px; - padding: 20px; - margin: 10px; - box-shadow: 0 4px 10px rgba(0,0,0,0.05); /* Light shadow for depth */ -} + background-color: #f7f7f7; + border-radius: 8px; + padding: 20px; + margin: 10px; + box-shadow: 0 4px 10px rgba(0,0,0,0.05); + } .search-bar { display: flex; margin-bottom: 20px; @@ -224,23 +276,30 @@ } .info-block { - background-color: #ffffff; - padding: 15px; - border-radius: 8px; - box-shadow: 0 2px 5px rgba(0,0,0,0.1); - margin-top: 15px; -} + background-color: #ffffff; + padding: 15px; + border-radius: 8px; + box-shadow: 0 2px 5px rgba(0,0,0,0.1); + margin-top: 15px; + } -.details-list { - list-style: none; - padding: 0; - margin-top: 10px; -} + .details-list { + list-style: none; + padding: 0; + margin-top: 10px; + } -.details-list li { - margin-bottom: 8px; - font-size: 16px; /* Larger font for readability */ -} + .details-list li { + margin-bottom: 8px; + font-size: 16px; + } + + .buttons { + display: flex; + flex-direction: row; + gap: 8px; + margin: 20px 0px 0px 10px !important; + } @@ -254,13 +313,46 @@ private int currentPage = 1; private int pageSize = 10; private int totalStudents; + private Dictionary images = new (); + private IEnumerable allFriends; + private IEnumerable incomingRequests; protected override async Task OnInitializedAsync() { - sentRequests = await FriendsService.GetSentFriendRequestsAsync(); + sentRequests = await FriendsService.GetSentFriendRequestsAsync(); + incomingRequests = await FriendsService.GetIncomingFriendRequestsAsync(); + allFriends = await FriendsService.GetAllFriendsAsync(IdentityService.GetCurrentUserId()); + await LoadStudents(); StateHasChanged(); + + + + var tasks = new List(); + foreach (var student in students) + { + tasks.Add(FetchImageAsync(student)); + } + + await Task.WhenAll(tasks); + } + + private async Task FetchImageAsync(StudentDto student) + { + var result = await MediaFilesService.GetFileAsync(student.ProfileImage); + if (result != null) + { + if (images.ContainsKey(student.Id)) + { + images[student.Id] = result.Base64Content; + } + else + { + images.Add(student.Id, result.Base64Content); + } + } } + @* private async Task LoadStudents() { var result = await FriendsService.GetAllStudentsAsync(currentPage, pageSize); totalStudents = result.Count(); @@ -268,25 +360,29 @@ students = result.ToList(); } } *@ - private async Task LoadStudents() - { + private async Task LoadStudents() { int maxPage = (int)Math.Ceiling((double)totalStudents / pageSize); if (currentPage > maxPage) currentPage = maxPage; if (currentPage < 1) currentPage = 1; var response = await FriendsService.GetAllStudentsAsync(currentPage, pageSize); - if (response != null) - { + if (response != null) { students = response.Results; totalStudents = response.Total; - } - else - { + StateHasChanged(); + await LoadImagesForStudents(students); + } else { students = new List(); } StateHasChanged(); } + private async Task LoadImagesForStudents(List students) { + images = new Dictionary(); // Clear previous images + var tasks = students.Select(student => FetchImageAsync(student)).ToList(); + await Task.WhenAll(tasks); + } + private string GetImage(string base64Image) { if (string.IsNullOrWhiteSpace(base64Image)) @@ -303,9 +399,14 @@ private void OnDetails(StudentDto selectedStudent) { student = selectedStudent; + StateHasChanged(); } private void SearchFriends() { + if (string.IsNullOrWhiteSpace(searchTerm)) + { + return; + } students = students.Where(s => s.FirstName.Contains(searchTerm, StringComparison.OrdinalIgnoreCase) || s.LastName.Contains(searchTerm, StringComparison.OrdinalIgnoreCase)).ToList(); } @@ -321,13 +422,13 @@ var currentUserId = IdentityService.GetCurrentUserId(); await FriendsService.InviteStudent(currentUserId, studentId); - var student = students.FirstOrDefault(s => s.Id == studentId); + var student = students.FirstOrDefault(s => s.Id == studentId); if (student != null) { student.InvitationSent = true; student.IsInvitationPending = true; } - sentRequests = await FriendsService.GetSentFriendRequestsAsync(); + sentRequests = await FriendsService.GetSentFriendRequestsAsync(); NotificationService.Notify(Radzen.NotificationSeverity.Success, "Invitation Sent", "The invitation has been successfully sent.", 10000); await JSRuntime.InvokeVoidAsync("playNotificationSound"); StateHasChanged(); @@ -335,15 +436,21 @@ private async Task SetPage(int page) { - Console.WriteLine($"Attempting to set page to {page}"); + @* Console.WriteLine($"Attempting to set page to {page}"); *@ if (page < 1 || page > Math.Ceiling((double)totalStudents / pageSize)) { - Console.WriteLine("Page number out of range."); + @* Console.WriteLine("Page number out of range."); *@ return; } currentPage = page; - Console.WriteLine($"Page set to {currentPage}"); + @* Console.WriteLine($"Page set to {currentPage}"); *@ + student = null; await LoadStudents(); - StateHasChanged(); + @* StateHasChanged(); *@ + } + + private void ViewDetails(Guid studentId) + { + NavigationManager.NavigateTo($"/student-details/{studentId}"); } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/SentRequests.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/SentRequests.razor index 41c341e5..0bd222d5 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/SentRequests.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/SentRequests.razor @@ -6,47 +6,266 @@ @inject IFriendsService FriendsService @using MiniSpace.Web.DTO @using MiniSpace.Web.Areas.Identity -@using MiniSpace.Services.Friends.Application.Dto; +@using MiniSpace.Services.Friends.Application.Dto @inject IIdentityService IdentityService @inject Radzen.NotificationService NotificationService +@inject IJSRuntime JSRuntime +@using MiniSpace.Web.Areas.MediaFiles +@inject IMediaFilesService MediaFilesService +@using MudBlazor -

Sent Friend Requests

+ -@if (sentRequests == null) -{ -

Loading...

-} -else if (sentRequests.Any()) -{ - - - - - - - - - - -} -else -{ -

No sent requests.

+@code { + private List _items = new List + { + new BreadcrumbItem("Home", href: "/", icon: Icons.Material.Filled.Home), + new BreadcrumbItem("Search", href: "/friends/search", icon: Icons.Material.Filled.Create), + new BreadcrumbItem("Requests", href: "/friends/requests", icon: Icons.Material.Filled.VideoLibrary), + new BreadcrumbItem("Sent Requests", href: "/friends/sent-requests", disabled: true, icon: Icons.Material.Filled.VideoLibrary), + }; } +

Sent Friend Requests

+ +
+
+ +
+ @if (sentRequests == null) + { +

Loading...

+ } + else if (filteredSentRequests.Any()) + { + @foreach (var request in filteredSentRequests) + { +
+ @if (images.ContainsKey(request.InviteeId)) + { + Invitee Image + } +
+
@request.InviteeName
+

Email: @request.InviteeEmail

+

Requested On: @request.RequestedAt.ToLocalTime().ToString("yyyy-MM-dd")

+

Status: @request.State

+ +
+
+ + } + } + else + { +

No sent requests.

+ } +
+
+
+ + + @code { + private string searchTerm; + private List students = new List(); private IEnumerable sentRequests; + private IEnumerable filteredSentRequests; + private Dictionary images = new(); protected override async Task OnInitializedAsync() { sentRequests = await FriendsService.GetSentFriendRequestsAsync(); - StateHasChanged(); + + var inviteeIds = sentRequests.Select(r => r.InviteeId).Distinct(); + var studentTasks = inviteeIds.Select(id => FriendsService.GetStudentAsync(id)); + + students = (await Task.WhenAll(studentTasks)).ToList(); + + var imageTasks = students.Select(student => FetchImageAsync(student.Id, student.ProfileImage)); + await Task.WhenAll(imageTasks); + + foreach (var request in sentRequests) + { + var student = students.FirstOrDefault(s => s.Id == request.InviteeId); + if (student != null) + { + request.InviteeName = $"{student.FirstName} {student.LastName}"; + request.InviteeEmail = student.Email; + if (images.ContainsKey(student.Id)) + { + request.InviteeImage = images[student.Id]; + } + } + } + + filteredSentRequests = sentRequests; + } + + private async Task FetchImageAsync(Guid inviteeId, Guid profileImage) + { + var result = await MediaFilesService.GetFileAsync(profileImage); + images[inviteeId] = result.Base64Content; + } + + private string GetImage(string base64Image) + { + if (string.IsNullOrWhiteSpace(base64Image)) + { + return "images/user_image.png"; + } + else + { + return $"data:image/jpeg;base64,{base64Image}"; + } + } + + private void SearchSentRequests() + { + filteredSentRequests = string.IsNullOrWhiteSpace(searchTerm) + ? sentRequests + : sentRequests.Where(x => x.InviteeName.Contains(searchTerm, StringComparison.OrdinalIgnoreCase)); } private void RedirectToDetails(Guid id) { NavigationManager.NavigateTo($"/student-details/{id}"); } + + private async Task WithdrawRequest(Guid inviteeId) + { + var inviterId = IdentityService.GetCurrentUserId(); + if (inviterId != Guid.Empty) + { + await FriendsService.WithdrawFriendRequestAsync(inviterId, inviteeId); + sentRequests = await FriendsService.GetSentFriendRequestsAsync(); + SearchSentRequests(); // Reapply search filter to update the UI + } + else + { + // Optionally show an error notification + NotificationService.Notify(Radzen.NotificationSeverity.Error, "Error", "Invalid user ID."); + } + } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Index.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Index.razor index fb16ed11..63270317 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Index.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Index.razor @@ -122,8 +122,8 @@ private bool enableSwipeGesture = true; private bool autocycle = true; private MudBlazor.Transition transition = MudBlazor.Transition.Slide; - private List images = new List { "images/mini_1.jpg", "images/pw_1.jpg", "images/pw_2.jpg" }; - private List images2 = new List { "images/students_1.jpg", "images/students_2.jpg", "images/students_3.jpg" }; + private List images = new List { "images/mini_1.webp", "images/pw_1.webp", "images/pw_2.webp" }; + private List images2 = new List { "images/students_1.webp", "images/students_2.webp", "images/students_3.webp" }; private List titles = new List { "Exploration", "Connection", "Sharing" }; private List descriptions = new List { "Explore new places and meet new people on MiniSpace.", diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Notifications/AllNotifications.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Notifications/AllNotifications.razor new file mode 100644 index 00000000..4f9e1d21 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Notifications/AllNotifications.razor @@ -0,0 +1,115 @@ +@page "/notifications/all" +@using MiniSpace.Web.Areas.Notifications +@inject INotificationsService NotificationsService +@inject NavigationManager NavigationManager +@using MiniSpace.Web.DTO.Notifications +@using Radzen +@using System.Linq +@inject IIdentityService IdentityService +@using MudBlazor + + +@code { + private List _items = new List + { + new BreadcrumbItem("Home", href: "/", icon: Icons.Material.Filled.Home), + new BreadcrumbItem("Notifications", href: "/notifications/all", icon: Icons.Material.Filled.Notifications) + }; +} + +
+

All Notifications

+
+ +@if (notifications == null) +{ +

Loading...

+} +else if (notifications.Any()) +{ + + + + + + + + + + + + + + + +} +else +{ +

No notifications found.

+} + +@code { + private List notifications; + private int currentPage = 1; + private int pageSize = 10; + private int totalNotifications; + + protected override async Task OnInitializedAsync() + { + await LoadNotifications(new LoadDataArgs { Skip = 0, Top = pageSize, OrderBy = "createdAt desc" }); + } + + + private async Task LoadNotifications(LoadDataArgs args) + { + + var skip = args.Skip ?? 0; + var top = args.Top ?? pageSize; + + currentPage = (skip / top) + 1; + pageSize = top; + + var userId = IdentityService.GetCurrentUserId(); + var response = await NotificationsService.GetNotificationsByUserAsync(userId, page: currentPage, pageSize: pageSize, sortOrder: "desc", status: null); + + if (response != null) + { + notifications = response.Results; + totalNotifications = response.Total; + StateHasChanged(); + } + } + + + private async Task UpdateNotificationStatus(Guid userId, Guid notificationId, string newStatus) + { + await NotificationsService.UpdateNotificationStatusAsync(userId, notificationId, newStatus); + notifications.Find(n => n.NotificationId == notificationId).Status = newStatus; + StateHasChanged(); + } + + private async Task DeleteNotification(Guid userId, Guid notificationId) + { + await NotificationsService.DeleteNotificationAsync(userId, notificationId); + notifications.RemoveAll(n => n.NotificationId == notificationId); + StateHasChanged(); + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Notifications/HistoryNotifications.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Notifications/HistoryNotifications.razor new file mode 100644 index 00000000..6c165b79 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Notifications/HistoryNotifications.razor @@ -0,0 +1,117 @@ +@page "/notifications/history" +@using MiniSpace.Web.Areas.Notifications +@inject INotificationsService NotificationsService +@inject NavigationManager NavigationManager +@using MiniSpace.Web.DTO.Notifications +@using Radzen +@using System.Linq +@inject IIdentityService IdentityService +@using MudBlazor + + +@code { + private List _items = new List + { + new BreadcrumbItem("Home", href: "/", icon: Icons.Material.Filled.Home), + new BreadcrumbItem("Notifications", href: "/notifications/all", icon: Icons.Material.Filled.Notifications), + new BreadcrumbItem("New Notifications", href: "/notifications/new", icon: Icons.Material.Filled.Notifications), + new BreadcrumbItem("Notifications History", href: "/notifications/history", disabled: true, icon: Icons.Material.Filled.Notifications) + }; +} + +
+

Notifications History

+
+ +@if (notifications == null) +{ +

Loading...

+} +else if (notifications.Any()) +{ + + + + + + + + + + + + + + + +} +else +{ +

No notifications found.

+} + +@code { + private List notifications; + private int currentPage = 1; + private int pageSize = 10; + private int totalNotifications; + + protected override async Task OnInitializedAsync() + { + await LoadNotifications(new LoadDataArgs { Skip = 0, Top = pageSize, OrderBy = "createdAt desc" }); + } + + + private async Task LoadNotifications(LoadDataArgs args) + { + + var skip = args.Skip ?? 0; + var top = args.Top ?? pageSize; + + currentPage = (skip / top) + 1; + pageSize = top; + + var userId = IdentityService.GetCurrentUserId(); + var response = await NotificationsService.GetNotificationsByUserAsync(userId, page: currentPage, pageSize: pageSize, sortOrder: "desc", status: "Read"); + + if (response != null) + { + notifications = response.Results; + totalNotifications = response.Total; + StateHasChanged(); + } + } + + + private async Task UpdateNotificationStatus(Guid userId, Guid notificationId, string newStatus) + { + await NotificationsService.UpdateNotificationStatusAsync(userId, notificationId, newStatus); + notifications.Find(n => n.NotificationId == notificationId).Status = newStatus; + StateHasChanged(); + } + + private async Task DeleteNotification(Guid userId, Guid notificationId) + { + await NotificationsService.DeleteNotificationAsync(userId, notificationId); + notifications.RemoveAll(n => n.NotificationId == notificationId); + StateHasChanged(); + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Notifications/NewNotifications.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Notifications/NewNotifications.razor new file mode 100644 index 00000000..2e799230 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Notifications/NewNotifications.razor @@ -0,0 +1,114 @@ +@page "/notifications/new" +@using MiniSpace.Web.Areas.Notifications +@inject INotificationsService NotificationsService +@inject NavigationManager NavigationManager +@using MiniSpace.Web.DTO.Notifications +@using Radzen +@using System.Linq +@inject IIdentityService IdentityService +@using MudBlazor + + +@code { + private List _items = new List + { + new BreadcrumbItem("Home", href: "/", icon: Icons.Material.Filled.Home), + new BreadcrumbItem("Notifications", href: "/notifications/all", icon: Icons.Material.Filled.Notifications), + new BreadcrumbItem("New Notifications", href: "/notifications/new", disabled: true, icon: Icons.Material.Filled.Notifications) + }; +} + +
+

Recent Notifications

+
+ +@if (notifications == null) +{ +

Loading...

+} +else if (notifications.Any()) +{ + + + + + + + + + + + + + + +} +else +{ +

No notifications found.

+} +@code { + private List notifications; + private int currentPage = 1; + private int pageSize = 10; + private int totalNotifications; + + protected override async Task OnInitializedAsync() + { + await LoadNotifications(new LoadDataArgs { Skip = 0, Top = pageSize, OrderBy = "createdAt desc" }); + } + + + private async Task LoadNotifications(LoadDataArgs args) + { + + var skip = args.Skip ?? 0; + var top = args.Top ?? pageSize; + + currentPage = (skip / top) + 1; + pageSize = top; + + var userId = IdentityService.GetCurrentUserId(); + var response = await NotificationsService.GetNotificationsByUserAsync(userId, page: currentPage, pageSize: pageSize, sortOrder: "desc", status: "Unread"); + + if (response != null) + { + notifications = response.Results; + totalNotifications = response.Total; + StateHasChanged(); + } + } + + + private async Task UpdateNotificationStatus(Guid userId, Guid notificationId, string newStatus) + { + await NotificationsService.UpdateNotificationStatusAsync(userId, notificationId, newStatus); + notifications.Find(n => n.NotificationId == notificationId).Status = newStatus; + StateHasChanged(); + } + + private async Task DeleteNotification(Guid userId, Guid notificationId) + { + await NotificationsService.DeleteNotificationAsync(userId, notificationId); + notifications.RemoveAll(n => n.NotificationId == notificationId); + StateHasChanged(); + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Notifications/Notification.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Notifications/Notification.razor new file mode 100644 index 00000000..52938f6a --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Notifications/Notification.razor @@ -0,0 +1,70 @@ +@page "/notification/{NotificationId:guid}" +@using MiniSpace.Web.Areas.Notifications +@inject INotificationsService NotificationsService +@using MiniSpace.Web.DTO.Notifications +@inject IIdentityService IdentityService +@using MudBlazor + + +@code { + private List _items = new List + { + new BreadcrumbItem("Home", href: "/", icon: Icons.Material.Filled.Home), + new BreadcrumbItem("Notifications", href: "/notifications/all", icon: Icons.Material.Filled.VideoLibrary), + new BreadcrumbItem("New notification", href: "/notifications/all", disabled: true, icon: Icons.Material.Filled.VideoLibrary), + new BreadcrumbItem("Notifications History", href: "/notifications/history", disabled: true, icon: Icons.Material.Filled.Notifications) + }; +} + +
+

Notification Details

+
+ +@if (notification != null) +{ +
+

Message: @RenderMessage(notification)

+

Date: @notification.CreatedAt.ToString("dd MMM yyyy")

+

Status: @notification.Status

+ +
+} +else +{ +

Loading notification details...

+} + +@code { + [Parameter] public Guid NotificationId { get; set; } + private NotificationDto notification; + + protected override async Task OnInitializedAsync() + { + var userId = IdentityService.GetCurrentUserId(); + notification = await NotificationsService.GetNotificationByIdAsync(userId, NotificationId); + if (notification.Status == "Unread") { + await UpdateNotificationStatus(userId, NotificationId, "Read"); + } + } + + private async Task UpdateNotificationStatus(Guid userId, Guid notificationId, string newStatus) + { + await NotificationsService.UpdateNotificationStatusAsync(userId, notificationId, newStatus); + notification.Status = newStatus; + StateHasChanged(); + } + + private MarkupString RenderMessage(NotificationDto notification) + { + if (!string.IsNullOrEmpty(notification.RelatedEntityId.ToString())) + { + return new MarkupString($"{notification.Message} View Profile"); + } + else + { + return new MarkupString(notification.Message); + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/Post.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/Post.razor index bd813320..b8217e05 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/Post.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/Post.razor @@ -3,6 +3,8 @@ @using MiniSpace.Web.DTO @using MiniSpace.Web.Areas.Posts @using MiniSpace.Web.Pages.Posts.Dialogs +@using MiniSpace.Web.Areas.Reactions +@using MiniSpace.Web.DTO.Enums @using Radzen @using AlignItems = Radzen.AlignItems @using DialogOptions = Radzen.DialogOptions @@ -11,6 +13,7 @@ @inject DialogService DialogService @inject IIdentityService IdentityService @inject IPostsService PostsService +@inject IReactionsService ReactionsService @inject NavigationManager NavigationManager @if (!pageInitialized) @@ -21,7 +24,7 @@ @if (pageInitialized) { - + @@ -70,14 +73,53 @@ Click="@(() => OpenDeletePostDialog(post.Id))" /> } + + + + + + Number of reactions + @(reactionsSummary.NumberOfReactions) + + + + + Dominant reaction + @(ReactionTypeExtensions.GetReactionText(reactionsSummary.DominantReaction)) + + + + - + + @if (pageInitialized && !reactions.Any()) + { +

No reactions have been added by students yet.

+ } + + +
@@ -97,7 +139,12 @@ private Guid studentId; private PostDto post = new(); private bool pageInitialized = false; - + + IEnumerable reactions; + + ReactionsSummaryDto reactionsSummary = new(); + List> reactionTypes = ReactionTypeExtensions.GenerateReactionPairs(); + protected override async Task OnInitializedAsync() { if (IdentityService.IsAuthenticated) @@ -105,6 +152,8 @@ studentId = IdentityService.GetCurrentUserId(); } post = await PostsService.GetPostAsync(new Guid(PostId)); + reactionsSummary = await ReactionsService.GetReactionsSummary(post.Id, ReactionContentType.Post); + reactions = await ReactionsService.GetReactions(post.Id, ReactionContentType.Post); pageInitialized = true; } @@ -119,6 +168,22 @@ return studentId == postDto.OrganizerId; } + private async void OnChange(int index) + { + if (!IdentityService.IsAuthenticated) + { + return; + } + + switch (index) + { + case 1: + reactions = await ReactionsService.GetReactions(post.Id, ReactionContentType.Post); + break; + } + StateHasChanged(); + } + private async Task OpenDeletePostDialog(Guid postId) { await DialogService.OpenAsync("Are you sure? This action cannot be undone!", @@ -129,4 +194,21 @@ AutoFocusFirstElement = false }); } + + private async void ReactionChanged(ReactionType? reactionType) + { + if (reactionsSummary.AuthUserReactionId != null) + { + await ReactionsService.DeleteReaction((Guid)reactionsSummary.AuthUserReactionId); + } + + if (reactionType != null) + { + await ReactionsService.CreateReaction(Guid.Empty, studentId, reactionType.ToString(), + post.Id, ReactionContentType.Post.ToString()); + } + + reactionsSummary = await ReactionsService.GetReactionsSummary(post.Id, ReactionContentType.Post); + StateHasChanged(); + } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/PostCreate.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/PostCreate.razor index 29865e97..ca423f87 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/PostCreate.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/PostCreate.razor @@ -1,12 +1,16 @@ @page "/events/{EventId}/posts/create" @using MiniSpace.Web.Areas.Identity @using MiniSpace.Web.Areas.Http +@using MiniSpace.Web.Areas.MediaFiles @using MiniSpace.Web.Areas.Posts @using MiniSpace.Web.DTO +@using MiniSpace.Web.DTO.Types @using MiniSpace.Web.Models.Posts @using Radzen +@using System.IO @inject IIdentityService IdentityService @inject IPostsService PostsService +@inject IMediaFilesService MediaFilesService @inject IErrorMapperService ErrorMapperService @inject NavigationManager NavigationManager @@ -50,6 +54,25 @@ } + + + + + @if (isUploading) + { + + + + } + + + Choose files to upload (max 3) + + + + @@ -72,11 +95,13 @@ private CreatePostModel createPostModel = new() { TextContent = "Lorem ipsum!", - MediaContent = "" }; private bool showError = false; private string errorMessage = string.Empty; private int publishInfo = 1; + private TaskCompletionSource clientChangeCompletionSource; + private bool isUploading = false; + private Dictionary images = new (); private static bool ValidateDate(DateTime dateTime) { @@ -88,7 +113,7 @@ if (IdentityService.IsAuthenticated && IdentityService.GetCurrentUserRole() == "organizer") { organizerId = IdentityService.GetCurrentUserId(); - + createPostModel.PostId = Guid.NewGuid(); createPostModel.EventId = new Guid(EventId); createPostModel.OrganizerId = organizerId; } @@ -96,10 +121,14 @@ private async Task HandleCreatePost() { - var response = await PostsService.CreatePostAsync(Guid.Empty, createPostModel.EventId, - createPostModel.OrganizerId, createPostModel.TextContent, createPostModel.MediaContent, + if (clientChangeCompletionSource != null) + { + await clientChangeCompletionSource.Task; + } + var response = await PostsService.CreatePostAsync(createPostModel.PostId, createPostModel.EventId, + createPostModel.OrganizerId, createPostModel.TextContent, images.Select(i => i.Value), publishInfo == 2 ? "ToBePublished" : "Published", - publishInfo == 2 ? createPostModel.PublishDate : null); + publishInfo == 2 ? createPostModel.PublishDate.ToUniversalTime() : null); if (response.ErrorMessage != null) { @@ -111,4 +140,62 @@ NavigationManager.NavigateTo($"/events/{EventId}"); } } + + async void OnClientChange(UploadChangeEventArgs args) + { + @* Console.WriteLine("Client-side upload changed"); *@ + clientChangeCompletionSource = new TaskCompletionSource(); + var uploadedImages = new Dictionary(); + isUploading = true; + + foreach (var file in args.Files) + { + StateHasChanged(); + if (images.TryGetValue(file.Name, out var imageId)) + { + uploadedImages.Add(file.Name, imageId); + continue; + } + + try + { + long maxFileSize = 10 * 1024 * 1024; + var stream = file.OpenReadStream(maxFileSize); + byte[] bytes = await ReadFully(stream); + var base64Content = Convert.ToBase64String(bytes); + var response = await MediaFilesService.UploadMediaFileAsync(createPostModel.PostId, + MediaFileContextType.Post.ToString(), IdentityService.UserDto.Id, + file.Name, file.ContentType, base64Content); + if (response.Content != null && response.Content.FileId != Guid.Empty) + { + uploadedImages.Add(file.Name, response.Content.FileId); + } + stream.Close(); + } + catch (Exception ex) + { + @* Console.WriteLine($"Client-side file read error: {ex.Message}"); *@ + } + finally + { + + } + } + isUploading = false; + StateHasChanged(); + images = uploadedImages; + clientChangeCompletionSource.SetResult(true); + } + + private static async Task ReadFully(Stream input) + { + byte[] buffer = new byte[16*1024]; + using MemoryStream ms = new MemoryStream(); + int read; + while ((read = await input.ReadAsync(buffer, 0, buffer.Length)) > 0) + { + ms.Write(buffer, 0, read); + } + return ms.ToArray(); + } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/PostUpdate.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/PostUpdate.razor index e0fac9be..0d92ed9d 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/PostUpdate.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/PostUpdate.razor @@ -82,7 +82,7 @@ postDto = await PostsService.GetPostAsync(new Guid(PostId)); updatePostModel.PostId = postDto.Id; updatePostModel.TextContent = postDto.TextContent; - updatePostModel.MediaContent = postDto.MediaContent; + updatePostModel.MediaFiles = postDto.MediaFiles; } pageInitialized = true; @@ -91,7 +91,7 @@ private async Task HandleUpdatePost() { var response = await PostsService.UpdatePostAsync(updatePostModel.PostId, - updatePostModel.TextContent, updatePostModel.MediaContent); + updatePostModel.TextContent, updatePostModel.MediaFiles); if (response.ErrorMessage != null) { diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/_Host.cshtml b/MiniSpace.Web/src/MiniSpace.Web/Pages/_Host.cshtml index fa3c2f0a..f0c75821 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/_Host.cshtml +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/_Host.cshtml @@ -43,11 +43,14 @@ } function playNotificationSound() { - var audio = new Audio('sounds/create-connection.mp3'); + var audio = new Audio('sounds/create-connection-new.wav'); audio.play(); } - + function playNotificationSoundNotificationsService() { + var audio = new Audio('sounds/new-notification.wav'); + audio.play(); + } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Shared/MainLayout.razor b/MiniSpace.Web/src/MiniSpace.Web/Shared/MainLayout.razor index 2dbc18f3..32323b18 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Shared/MainLayout.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Shared/MainLayout.razor @@ -17,10 +17,10 @@ TODO: do not forget about the code snippet chaned ❕❕❕ @if (IsUserAuthenticated && StudentsService.StudentDto.State == "valid") *@ - @if (_isUserAuthenticated ) + @* @if (_isUserAuthenticated ) { - } + } *@
@@ -43,7 +43,7 @@ - +
@@ -59,9 +59,20 @@ }
+ + @if (_isUserAuthenticated ) + { + +
+ +
+ + } + +
@* TODO: do not forget about the code snippet chaned ❕❕❕ @@ -69,17 +80,17 @@ *@ @if (IsUserAuthenticated) { - - + + + Click="@(() => NavigationManager.NavigateTo(""))" /> + Click="@(() => NavigationManager.NavigateTo("events/follow"))"/> @if (IdentityService.GetCurrentUserRole() == "organizer") @@ -93,7 +104,13 @@ - + + + + + + + @if (IdentityService.GetCurrentUserRole() == "admin") @@ -109,11 +126,32 @@ } - -
- @Body +
+
+
+
+
+ +
+
+ + @* *@ +
+ @Body +
+ @*
*@ +
+
+ + @if(_isUserAuthenticated) + { +
+ +
+ }
- + +
@@ -132,6 +170,8 @@ private bool _isUserAuthenticated; + private string currentRoute; + protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { _isUserAuthenticated = await IdentityService.CheckIfUserIsAuthenticated(); @@ -150,14 +190,14 @@ public async Task CheckAuthentication() { _isUserAuthenticated = await IdentityService.CheckIfUserIsAuthenticated(); - Console.WriteLine($"IsUserAuthenticated: {_isUserAuthenticated}"); + @* Console.WriteLine($"IsUserAuthenticated: {_isUserAuthenticated}"); *@ return _isUserAuthenticated; } public bool IsUserAuthenticated => _isUserAuthenticated; async Task SignOut() { - Console.WriteLine("Signing out..."); + @* Console.WriteLine("Signing out..."); *@ await localStorage.RemoveItemAsync("accessToken"); await localStorage.RemoveItemAsync("jwtDto"); NavigationManager.NavigateTo("signin", forceLoad: true); diff --git a/MiniSpace.Web/src/MiniSpace.Web/Shared/NotificationComponent.razor b/MiniSpace.Web/src/MiniSpace.Web/Shared/NotificationComponent.razor new file mode 100644 index 00000000..7cb166e5 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Shared/NotificationComponent.razor @@ -0,0 +1,97 @@ +@using MiniSpace.Web.Areas.Notifications +@inject INotificationsService NotificationsService +@inject IIdentityService IdentityService +@using MiniSpace.Web.DTO.Notifications +@using System.Threading +@inject IJSRuntime JSRuntime +@inject NavigationManager NavigationManager + +
+

Notifications

+ +
+ @if (notifications != null && notifications.Any()) + { +
    + @foreach (var notification in notifications) + { +
  • + @notification.Message + @notification.CreatedAt.ToString("g") +
  • + } +
+ } + else + { +

No new notifications.

+ } +
+
+ +@code { + private List previousNotifications = new List(); + private List notifications; + private Timer timer; + private DateTime lastCheckedTime = DateTime.MinValue; + + protected override async Task OnInitializedAsync() + { + await LoadNotifications(); + // Setup a timer to refresh notifications every 15 seconds + timer = new Timer(new TimerCallback(_ => InvokeAsync(LoadNotifications)), null, 0, 15000); + } + + private async Task LoadNotifications() + { + try + { + var userId = IdentityService.GetCurrentUserId(); + var paginatedResponse = await NotificationsService.GetNotificationsByUserAsync(userId, status: "Unread"); + var latestNotifications = paginatedResponse.Results; + + if (latestNotifications.Any(n => n.CreatedAt > lastCheckedTime)) + { + PlayNotificationSound(); + lastCheckedTime = DateTime.Now; + } + + notifications = latestNotifications; + StateHasChanged(); + } + catch (Exception ex) + { + Console.WriteLine($"Error loading notifications: {ex.Message}"); + } + } + + + private bool isNewNotificationReceived() + { + if (previousNotifications == null || !previousNotifications.Any()) + { + return notifications.Any(); + } + else + { + return notifications.Any(n => !previousNotifications.Any(p => p.NotificationId == n.NotificationId)); + } + } + + private void PlayNotificationSound() + { + JSRuntime.InvokeVoidAsync("playNotificationSoundNotificationsService"); + } + + private void NavigateToNotificationDetail(Guid notificationId) + { + NavigationManager.NavigateTo($"/notification/{notificationId}"); + } + + public void Dispose() + { + timer?.Dispose(); + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Startup.cs b/MiniSpace.Web/src/MiniSpace.Web/Startup.cs index 09227b19..dcbcdbf0 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Startup.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Startup.cs @@ -23,6 +23,10 @@ using MiniSpace.Web.Areas.Friends; using Microsoft.AspNetCore.Components.Authorization; using Blazored.LocalStorage; +using MiniSpace.Web.Areas.Notifications; +using MiniSpace.Web.Areas.MediaFiles; +using MiniSpace.Web.Areas.Reactions; + namespace MiniSpace.Web { @@ -65,9 +69,12 @@ public void ConfigureServices(IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); } diff --git a/MiniSpace.Web/src/MiniSpace.Web/wwwroot/css/site.css b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/css/site.css index 9508286c..464398ff 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/wwwroot/css/site.css +++ b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/css/site.css @@ -1,6 +1,12 @@ @import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); @import url('https://cdn.jsdelivr.net/npm/mudblazor/css/mudblazor.min.css'); +:root { + --header-color: #102338; + --link-color: #0366d6; + --primary-btn-bg: #1b6ec2; + --primary-btn-border: #1861ac; +} html, body { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; } @@ -162,7 +168,7 @@ app { } header { - background-color: #2F4E6F; + background-color: var(--header-color);; color: white; padding: 0.5rem 1rem; display: flex; @@ -244,7 +250,7 @@ html, body { .logo-container { display: inline-block; padding: 5px 10px; - background-color: #2F4E6F; + background-color: var(--header-color); border-radius: 5px; margin-right: 15px; } @@ -268,7 +274,7 @@ html, body { } .rz-navigation-menu { - background-color: #2F4E6F !important; + background-color: var(--header-color) !important; color: #fff !important; transition: background-color 0.3s ease; } @@ -303,7 +309,8 @@ html, body { } .rz-menu { - background-color: #2F4E6F !important; + z-index: 100 !important; + background-color: var(--header-color) !important; color: #fff !important; height: 50px; } @@ -327,27 +334,32 @@ html, body { } .rz-menu-item { - color: white; padding: 10px; cursor: pointer; text-align: center; border: none; background: none; - color: #fff !important; + color: #000 !important; } @media (max-width: 767.98px) { .rz-menu-stack { flex-direction: column; align-items: center; - width: 100%; + width: 100% !important; + + background-color: var(--header-color) !important; } .rz-menu-item { - width: 100%; + width: 100%!important; padding: 8px 0; } + .rz-navigation-item-link { + width: 100% !important; + } + /*.rz-display-flex {*/ /* display: block !important;*/ /*}*/ @@ -391,4 +403,140 @@ html, body { /*.rz-layout .rz-body {*/ /* overflow: unset !important;*/ -/*}*/ \ No newline at end of file +/*}*/ +@media (max-width: 768px) { + .split-container { + flex-direction: column; + height: auto; + } + + + .left-side, .right-side { + width: 100%; + flex: none !important; + } + + .left-side { + height: 10vh !important; + } + + video { + height: 10vh !important; /* Adjust video height on smaller screens */ + } + + .form-container { + margin-top: -50px; /* Overlap form on video slightly */ + border-radius: 10px 10px 0 0; + } +} + + +.sign-in-up-pre-info { + padding: 3rem 1rem 3rem 1rem; +} + +.sign-in-up-pre-info p { + margin: 2rem 1rem 2rem 2rem !important; +} + + +.breadcrumbs-container { + padding: 12px 24px; + background-color: #f9f9f9; + border-radius: 5px; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); + margin: 20px !important; + margin-top: 30px !important; +} + +.custom-breadcrumbs .mud-breadcrumbs-item { + color: #505050; + font-size: 16px; + font-weight: 500; +} + +.custom-breadcrumbs .mud-breadcrumbs-item:hover { + color: #007BFF; + text-decoration: none; +} + +.custom-breadcrumbs .mud-breadcrumbs-divider { + color: #C0C0C0; +} + +.custom-breadcrumbs .mud-breadcrumbs-item:last-child { + color: #A0A0A0; + pointer-events: none; +} + +.custom-breadcrumbs .mud-icon { + margin-right: 8px; +} + +.custom-breadcrumbs .mud-svg-icon { + fill: #505050; +} + +.custom-breadcrumbs .mud-svg-icon:hover { + fill: #007BFF; +} + + + +.custom-breadcrumbs .mud-breadcrumbs-item:focus { + outline: none; + box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.5); +} + + +@media (max-width: 768px) { + .right-panel { + display: none; + } + + .notification-component { + display: none; + } +} + +.header-services { + /* background-color: #152d4d; */ /* <----- more powerful */ + background-color: #30445F; +} + +.header-services .rz-sidebar-toggle { + background-color: #30445F; + color: white; + border: none; + padding: 10px 15px; + font-size: 16px; + border-radius: 5px; + box-shadow: 0 2px 4px rgba(0,0,0,0.2); + transition: background-color 0.3s, transform 0.3s; +} + +.header-services .rz-sidebar-toggle:hover, .header-services .rz-sidebar-toggle:focus { + background-color: #102338; + transform: scale(1.05); + outline: none; +} + +.header-services .rz-sidebar-toggle:active { + background-color: #30445F; + transform: scale(0.95); +} + +.notification-item { + padding: 8px 12px; + border-bottom: 1px solid #dee2e6; + transition: background-color 0.3s, box-shadow 0.3s; +} +.notification-item:hover { + background-color: #f8f9fa; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); + cursor: pointer; +} + +.notifications-page-title { + margin: 2rem !important; +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/wwwroot/images/mini_1.webp b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/images/mini_1.webp new file mode 100644 index 00000000..89443001 Binary files /dev/null and b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/images/mini_1.webp differ diff --git a/MiniSpace.Web/src/MiniSpace.Web/wwwroot/images/pw_1.webp b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/images/pw_1.webp new file mode 100644 index 00000000..5a6b4295 Binary files /dev/null and b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/images/pw_1.webp differ diff --git a/MiniSpace.Web/src/MiniSpace.Web/wwwroot/images/pw_2.webp b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/images/pw_2.webp new file mode 100644 index 00000000..0bb1d45b Binary files /dev/null and b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/images/pw_2.webp differ diff --git a/MiniSpace.Web/src/MiniSpace.Web/wwwroot/images/students_1.webp b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/images/students_1.webp new file mode 100644 index 00000000..11ca8100 Binary files /dev/null and b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/images/students_1.webp differ diff --git a/MiniSpace.Web/src/MiniSpace.Web/wwwroot/images/students_2.webp b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/images/students_2.webp new file mode 100644 index 00000000..ed77a9cf Binary files /dev/null and b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/images/students_2.webp differ diff --git a/MiniSpace.Web/src/MiniSpace.Web/wwwroot/images/students_3.webp b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/images/students_3.webp new file mode 100644 index 00000000..8158f38f Binary files /dev/null and b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/images/students_3.webp differ diff --git a/MiniSpace.Web/src/MiniSpace.Web/wwwroot/sounds/create-connection-new.wav b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/sounds/create-connection-new.wav new file mode 100644 index 00000000..88a18e15 Binary files /dev/null and b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/sounds/create-connection-new.wav differ diff --git a/MiniSpace.Web/src/MiniSpace.Web/wwwroot/sounds/new-notification.wav b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/sounds/new-notification.wav new file mode 100644 index 00000000..d2a1ee2b Binary files /dev/null and b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/sounds/new-notification.wav differ diff --git a/MiniSpace.Web/src/MiniSpace.Web/wwwroot/videos/index1.mp4 b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/videos/video-component/index1.mp4 similarity index 100% rename from MiniSpace.Web/src/MiniSpace.Web/wwwroot/videos/index1.mp4 rename to MiniSpace.Web/src/MiniSpace.Web/wwwroot/videos/video-component/index1.mp4 diff --git a/MiniSpace.Web/src/MiniSpace.Web/wwwroot/videos/video-component/video_1.mp4 b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/videos/video-component/video_1.mp4 new file mode 100644 index 00000000..614a3878 Binary files /dev/null and b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/videos/video-component/video_1.mp4 differ diff --git a/MiniSpace.Web/src/MiniSpace.Web/wwwroot/videos/video-component/video_2.mp4 b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/videos/video-component/video_2.mp4 new file mode 100644 index 00000000..227d10a5 Binary files /dev/null and b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/videos/video-component/video_2.mp4 differ diff --git a/MiniSpace/compose/services.yml b/MiniSpace/compose/services.yml index 86000600..18f21267 100644 --- a/MiniSpace/compose/services.yml +++ b/MiniSpace/compose/services.yml @@ -75,6 +75,15 @@ services: networks: - minispace + mediafiles-service: + image: adrianvsaint/minispace.services.mediafiles:latest + container_name: mediafiles-service + restart: unless-stopped + ports: + - 5014:80 + networks: + - minispace + organizations-service: image: adrianvsaint/minispace.services.organizations:latest container_name: organizations-service @@ -83,6 +92,15 @@ services: - 5015:80 networks: - minispace + + notifications-service: + image: adrianvsaint/minispace.services.notifications:latest + container_name: notifications-service + restart: unless-stopped + ports: + - 5006:80 + networks: + - minispace web: image: adrianvsaint/minispace.web:latest diff --git a/MiniSpace/scripts/dockerize-all.sh b/MiniSpace/scripts/dockerize-all.sh index f2889fe3..98a670f5 100755 --- a/MiniSpace/scripts/dockerize-all.sh +++ b/MiniSpace/scripts/dockerize-all.sh @@ -10,7 +10,9 @@ directories=( "MiniSpace.Services.Reactions" "MiniSpace.Services.Posts" "MiniSpace.Services.Comments" + "MiniSpace.Services.MediaFiles" "MiniSpace.Services.Organizations" + "MiniSpace.Services.Notifications" ) for dir in "${directories[@]}"; do