Skip to content

Dev #21

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 68 commits into from
Jun 27, 2024
Merged

Dev #21

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
93c3b45
Implemented ReservationCreatedEvent, Added NOT WORKING RedisReservati…
lolgame99 Jun 4, 2024
e53fc92
Fixed Reservation BaseEvent parsing, DomainEvent parsing still 💀💀💀
lolgame99 Jun 4, 2024
34a884d
Fixed ReservationCreatedEvent
lolgame99 Jun 4, 2024
80d5987
Implemented new PlayOfferEvents, Made EventParser return generic, Sta…
lolgame99 Jun 4, 2024
cedba45
added properties to Member
CengoleFHV Jun 4, 2024
5bcefe2
Refactored ClubRepository UpdateEntityAsync to ClubEventHandler, Adju…
lolgame99 Jun 5, 2024
fb4b044
Renamed EntityBaseEvents for clearer distinction
lolgame99 Jun 5, 2024
7647258
Refactored MemberRepository UpdateEntityAsync to MemberEventHandler, …
lolgame99 Jun 5, 2024
17017c5
Added ReadEventRepository to ReservationEventHandler
lolgame99 Jun 5, 2024
323de1b
Refactored PlayOfferRepository UpdateEntityAsync to PlayOfferEventHan…
lolgame99 Jun 5, 2024
a65ec04
Implemented ReservationLimitExceeded event
lolgame99 Jun 5, 2024
9563ee7
Fixed RedisClubStreamService
lolgame99 Jun 5, 2024
28ac3bb
Merge pull request #13 from THC-Software/reservation_saga
CengoleFHV Jun 5, 2024
abedb21
added properties to Member
CengoleFHV Jun 4, 2024
ad9fd81
expanded Member props and added MEMBER_UPDATED Event
CengoleFHV Jun 5, 2024
7394b9e
Merge remote-tracking branch 'origin/projection_extension' into proje…
CengoleFHV Jun 5, 2024
5497c10
changed fom updatedEvent to Email and FullName Event
CengoleFHV Jun 5, 2024
316a889
Implemented Reservation projection
lolgame99 Jun 6, 2024
9db900c
Implemented Court Model, Court DomainEvents, CourtRepository incl. Tests
lolgame99 Jun 6, 2024
4e248e2
Added RedisCourtStreamService
lolgame99 Jun 6, 2024
b91374a
Fixed RedisStreamReader for court and reservation
lolgame99 Jun 6, 2024
efdf21f
Merge pull request #14 from THC-Software/reservation_court_projection
CengoleFHV Jun 6, 2024
93bff8c
added properties to Member
CengoleFHV Jun 4, 2024
e0bc0f3
expanded Member props and added MEMBER_UPDATED Event
CengoleFHV Jun 5, 2024
eaba781
changed fom updatedEvent to Email and FullName Event
CengoleFHV Jun 5, 2024
f4a0375
Merge branch 'projection_extension' of https://github.com/THC-Softwar…
CengoleFHV Jun 6, 2024
9baa887
added Club Name Changed Event
CengoleFHV Jun 6, 2024
a297081
Implemented WriteEventRepository, Added EventRepository IntegrationTests
lolgame99 Jun 7, 2024
d6b3e6c
Implemented PlayOfferCancelled event creation on club/member lock
lolgame99 Jun 7, 2024
35289fd
Adjusted Club/Member EventHandler tests, Added function to WriteEvent…
lolgame99 Jun 7, 2024
277cee0
Merge pull request #15 from THC-Software/writeEventRepo_lockedEvent_r…
CengoleFHV Jun 7, 2024
7c68828
added properties to Member
CengoleFHV Jun 4, 2024
9e41f2e
expanded Member props and added MEMBER_UPDATED Event
CengoleFHV Jun 5, 2024
5d29c3e
changed fom updatedEvent to Email and FullName Event
CengoleFHV Jun 5, 2024
1f04ba9
expanded Member props and added MEMBER_UPDATED Event
CengoleFHV Jun 5, 2024
107660c
changed fom updatedEvent to Email and FullName Event
CengoleFHV Jun 5, 2024
b0d6332
added Club Name Changed Event
CengoleFHV Jun 6, 2024
a71eb09
Merge branch 'projection_extension' of https://github.com/THC-Softwar…
CengoleFHV Jun 7, 2024
c512c3d
fixed tests
CengoleFHV Jun 7, 2024
1c99417
Merge branch 'dev' into projection_extension
CengoleFHV Jun 7, 2024
a148ea3
fixed tests (again)
CengoleFHV Jun 7, 2024
ce1cdbe
added Unit Tests
CengoleFHV Jun 7, 2024
f3a7093
added Handles for new Member and Club Events
CengoleFHV Jun 7, 2024
a443b49
Merge pull request #16 from THC-Software/projection_extension
lolgame99 Jun 7, 2024
6c858ef
Renamed redis container
lolgame99 Jun 17, 2024
55f09c5
Ensure DB created in production context
lolgame99 Jun 17, 2024
a29da27
Added Query to get PlayOffers by CreatorName
lolgame99 Jun 17, 2024
0e6d99d
Refactored GET Endpoints to accept participantId and clubId
lolgame99 Jun 17, 2024
b72f466
Added DTOs for entities
lolgame99 Jun 17, 2024
b53927b
Refactored GetByClubIdQuery to return DTOs
lolgame99 Jun 17, 2024
f1f6153
Refactored missing GET Endpoints to return DTOs
lolgame99 Jun 17, 2024
dae15da
Implemented automatic cancelation of PlayOffer when its Reservation i…
lolgame99 Jun 17, 2024
7132620
Refactored PlayOfferRepository, Runtime optimization
lolgame99 Jun 17, 2024
e9a9703
Merge pull request #17 from THC-Software/query_refactoring
CengoleFHV Jun 17, 2024
5a4d4e8
Merge branch 'dev' into reservation_cancelation_fix
CengoleFHV Jun 17, 2024
0253914
Merge pull request #18 from THC-Software/reservation_cancelation_fix
CengoleFHV Jun 17, 2024
bf15f83
Removed list from courtId in Reservation
lolgame99 Jun 22, 2024
05d8a70
Refactored Reservation LimitExceeded & Rejected Events to reflect new…
lolgame99 Jun 22, 2024
66faff8
Added JWT athentication infrastructure
lolgame99 Jun 22, 2024
5a12bc1
Added jwt authentication for queries
lolgame99 Jun 22, 2024
2085277
Added authorization checks to commands
lolgame99 Jun 22, 2024
f5452d3
Fixed integration tests
lolgame99 Jun 23, 2024
9cd6767
Merge pull request #19 from THC-Software/jwt_implementation
CengoleFHV Jun 24, 2024
ff42138
Changed controller base url
lolgame99 Jun 25, 2024
ed7241f
added ConcurrencyCheck for CancelPlayOfferEvent
CengoleFHV Jun 26, 2024
1719554
added concurrency Check to CreatePlayOfferEvent
CengoleFHV Jun 26, 2024
27f65cd
added Concurrency Check on JoinPLayerOfferEvent
CengoleFHV Jun 26, 2024
4e871bc
Merge pull request #20 from THC-Software/optimistic-locking
lolgame99 Jun 26, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Application/Commands/CancelPlayOfferCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

namespace PlayOfferService.Application.Commands;

public record CancelPlayOfferCommand(Guid PlayOfferId) : IRequest<Task>
public record CancelPlayOfferCommand(Guid PlayOfferId, Guid MemberId) : IRequest<Task>
{
}
2 changes: 1 addition & 1 deletion Application/Commands/CreatePlayOfferCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
using PlayOfferService.Domain.Models;

namespace PlayOfferService.Application.Commands;
public record CreatePlayOfferCommand(PlayOfferDto PlayOfferDto) : IRequest<Guid>
public record CreatePlayOfferCommand(CreatePlayOfferDto CreatePlayOfferDto, Guid CreatorId, Guid ClubId) : IRequest<Guid>
{
}
2 changes: 1 addition & 1 deletion Application/Commands/JoinPlayOfferCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
using PlayOfferService.Domain.Models;

namespace PlayOfferService.Application.Commands;
public record JoinPlayOfferCommand(JoinPlayOfferDto JoinPlayOfferDto) : IRequest<Task>
public record JoinPlayOfferCommand(JoinPlayOfferDto JoinPlayOfferDto, Guid MemberId) : IRequest<Task>
{
}
121 changes: 103 additions & 18 deletions Application/Controllers/PlayOfferController.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
using System.Security.Claims;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using PlayOfferService.Application.Commands;
using PlayOfferService.Application.Exceptions;
using PlayOfferService.Application.Queries;
using PlayOfferService.Domain.Models;

namespace PlayOfferService.Application.Controllers;

[ApiController]
[Route("api")]
[Route("api/playoffers")]
public class PlayOfferController : ControllerBase
{

Expand All @@ -18,49 +21,114 @@
_mediator = mediator;
}

/// <summary>
/// Retrieve all Play Offers of the logged in users club
/// </summary>
/// <returns>Play offers with a matching club id</returns>
/// <response code="200">Returns a list of Play offers matching the query params</response>
/// <response code="204">No Play offer with matching properties was found</response>
[HttpGet]
[Authorize]
[Route("club")]
[ProducesResponseType(typeof(IEnumerable<PlayOfferDto>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ActionResult), StatusCodes.Status204NoContent)]
[Consumes("application/json")]
[Produces("application/json")]
public async Task<ActionResult<IEnumerable<PlayOfferDto>>> GetByClubIdAsync()
{
var clubId = Guid.Parse(User.Claims.First(c => c.Type == "tennisClubId").Value);
var result = await _mediator.Send(new GetPlayOffersByClubIdQuery(clubId));

if (result.Count() == 0)
return NoContent();

return Ok(result);
}

///<summary>
///Retrieve all Play Offers matching the query params
///Retrieve all Play Offers of a logged in user
///</summary>
///<param name="playOfferId">The id of the play offer</param>
///<param name="creatorId">The id of the creator of the play offer</param>
///<param name="clubId">The id of the club of the play offer</param>
///<returns>Play offer with a matching id</returns>
///<response code="200">Returns a Play offer matching the query params</response>
///<returns>List of Play offers with where given member is creator or opponent</returns>
///<response code="200">Returns a list of Play offers matching the query params</response>
///<response code="204">No Play offer with matching properties was found</response>
[HttpGet]
[Authorize]
[Route("participant")]
[ProducesResponseType(typeof(IEnumerable<PlayOffer>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ActionResult), StatusCodes.Status204NoContent)]
[Consumes("application/json")]
[Produces("application/json")]
public async Task<ActionResult<IEnumerable<PlayOfferDto>>> GetByParticipantIdAsync()
{
var participantId = Guid.Parse(User.FindFirst(ClaimTypes.NameIdentifier).Value);

Check warning on line 63 in Application/Controllers/PlayOfferController.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.
var result = await _mediator.Send(new GetPlayOffersByParticipantIdQuery(participantId));

if (result.Count() == 0)
return NoContent();

return Ok(result);
}

///<summary>
///Get all Play offers created by a member with a matching name
///</summary>
///<param name="creatorName">Name of the creator in the format '[FirstName] [LastName]', '[FirstName]' or '[LastName]'</param>
///<returns>A list of Play offers with a matching id</returns>
///<response code="200">Returns a List of Play offers with creator matching the query params</response>
///<response code="204">No Play offers with matching creator was found</response>
[HttpGet]
[Authorize]
[Route("search")]
[ProducesResponseType(typeof(IEnumerable<PlayOffer>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ActionResult), StatusCodes.Status204NoContent)]
[Consumes("application/json")]
[Produces("application/json")]
public async Task<ActionResult<IEnumerable<PlayOffer>>> GetByIdAsync([FromQuery] Guid? playOfferId, [FromQuery] Guid? creatorId, [FromQuery] Guid? clubId)
public async Task<ActionResult<IEnumerable<PlayOfferDto>>> GetByCreatorNameAsync([FromQuery] string creatorName)
{
var result = await _mediator.Send(new GetPlayOffersByIdQuery(playOfferId, creatorId, clubId));
IEnumerable<PlayOfferDto> result;
try
{
result = await _mediator.Send(new GetPlayOffersByCreatorNameQuery(creatorName));
}
catch (Exception e)
{
return BadRequest(e.Message);
}

if (result.Count() == 0)
return NoContent();

return Ok(result);
}



///<summary>
///Create a new Play Offer
///Create a new Play Offer for the logged in user
///</summary>
///<param name="playOfferDto">The Play Offer to create</param>
///<param name="createPlayOfferDto">The Play Offer to create</param>
///<returns>The newly created Play offer</returns>
///<response code="200">Returns the id of the created Play Offer</response>
///<response code="400">Invalid Play Offer structure</response>
///<response code="401">Only members can create Play Offers</response>
[HttpPost]
[Authorize]
[ProducesResponseType(typeof(PlayOffer), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ActionResult), StatusCodes.Status400BadRequest)]
[Consumes("application/json")]
[Produces("application/json")]
public async Task<ActionResult<PlayOffer>> Create(PlayOfferDto playOfferDto)
public async Task<ActionResult<PlayOffer>> Create(CreatePlayOfferDto createPlayOfferDto)
{
if (User.Claims.First(c => c.Type == "groups").Value != "MEMBER")
return Unauthorized("Only members can create Play Offers!");

var creatorId = Guid.Parse(User.FindFirst(ClaimTypes.NameIdentifier)!.Value);
var clubId = Guid.Parse(User.FindFirst("tennisClubId")!.Value);

Guid result;
try
{
result = await _mediator.Send(new CreatePlayOfferCommand(playOfferDto));
result = await _mediator.Send(new CreatePlayOfferCommand(createPlayOfferDto, creatorId, clubId));
}
catch (Exception e)
{
Expand All @@ -71,22 +139,33 @@
}

///<summary>
///Cancels a Play Offer with a matching id
///Cancels a Play Offer with a matching id of the logged in user
///</summary>
///<param name="playOfferId">The id of the Play Offer to cancel</param>
///<returns>Nothing</returns>
///<response code="200">The Play Offer with the matching id was cancelled</response>
///<response code="400">No Play Offer with matching id found</response>
///<response code="401">Only creator can cancel Play Offers</response>
[HttpDelete]
[Authorize]
[ProducesResponseType(typeof(ActionResult), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ActionResult), StatusCodes.Status400BadRequest)]
[Consumes("application/json")]
[Produces("application/json")]
public async Task<ActionResult> Delete(Guid playOfferId)
{
if (User.Claims.First(c => c.Type == "groups").Value != "MEMBER")
return Unauthorized("Only members can cancel Play Offers!");

var memberId = Guid.Parse(User.FindFirst(ClaimTypes.NameIdentifier)!.Value);

try
{
await _mediator.Send(new CancelPlayOfferCommand(playOfferId));
await _mediator.Send(new CancelPlayOfferCommand(playOfferId, memberId));
}
catch (AuthorizationException e)
{
return Unauthorized(e.Message);
}
catch (Exception e)
{
Expand All @@ -97,23 +176,29 @@
}

///<summary>
///Adds a given opponentId to a Play Offer and creates a reservation
///Logged in user joins a Play Offer with a matching playOfferId
///</summary>
///<param name="joinPlayOfferDto">The opponentId to add to the Play Offer with the matching playOfferId</param>
///<returns>Nothing</returns>
///<response code="200">The opponentId was added to the Play Offer with the matching playOfferId</response>
///<response code="400">No playOffer with a matching playOfferId found</response>
///<response code="401">Only members can join Play Offers</response>
[HttpPost]
[Route("/join")]
[Authorize]
[Route("join")]
[ProducesResponseType(typeof(ActionResult), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ActionResult), StatusCodes.Status400BadRequest)]
[Consumes("application/json")]
[Produces("application/json")]
public async Task<ActionResult> Join(JoinPlayOfferDto joinPlayOfferDto)
{
if (User.Claims.First(c => c.Type == "groups").Value != "MEMBER")
return Unauthorized("Only members can join Play Offers!");

var memberId = Guid.Parse(User.FindFirst(ClaimTypes.NameIdentifier)!.Value);
try
{
await _mediator.Send(new JoinPlayOfferCommand(joinPlayOfferDto));
await _mediator.Send(new JoinPlayOfferCommand(joinPlayOfferDto, memberId));
}
catch (Exception e)
{
Expand Down
35 changes: 29 additions & 6 deletions Application/EventParser.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,30 @@
using System.Text.Json;
using System.Text.Json.Nodes;
using PlayOfferService.Domain.Events;
using PlayOfferService.Domain.Events.Reservation;

namespace PlayOfferService.Application;

public class EventParser
{
public static BaseEvent ParseEvent(JsonNode jsonEvent)
public static T ParseEvent<T>(JsonNode jsonEvent) where T : BaseEvent, new()
{
var originalEventData = JsonNode.Parse(jsonEvent["eventData"].GetValue<string>());
JsonNode? originalEventData = null;
DateTime timestamp = DateTime.UtcNow;

// If eventData is JsonValue (escaped as string), parse it
if (jsonEvent["eventData"] is JsonValue)
{
originalEventData = JsonNode.Parse(jsonEvent["eventData"].GetValue<string>());

Check warning on line 18 in Application/EventParser.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.
timestamp = DateTime.Parse(jsonEvent["timestamp"].GetValue<string>()).ToUniversalTime();

Check warning on line 19 in Application/EventParser.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.
}
else // If eventData is already a JsonNode, just use it --> court_service
{
originalEventData = jsonEvent["eventData"];
timestamp = DateTimeOffset.FromUnixTimeMilliseconds(jsonEvent["timestamp"]["$date"].GetValue<long>())

Check warning on line 24 in Application/EventParser.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.
.UtcDateTime;
}

// We need to add the eventType to the event data so we can deserialize it correctly
// Since the discriminator needs to be at the first position in the JSON object, we need to create a new object
// because JsonNode doesn't allow us to insert at the beginning of the object
Expand All @@ -18,14 +33,22 @@
newEventData["eventType"] = jsonEvent["eventType"].GetValue<string>();
foreach (var kvp in originalEventData.AsObject())
{
newEventData[kvp.Key] = kvp.Value.DeepClone();
if (kvp.Value is JsonObject && kvp.Value?["$date"] != null)
{
newEventData[kvp.Key] = DateTimeOffset.FromUnixTimeMilliseconds(kvp.Value["$date"]
.GetValue<long>()).UtcDateTime;
}
else
{
newEventData[kvp.Key] = kvp.Value.DeepClone();
}
}
return new BaseEvent

return new T
{
EventId = Guid.Parse(jsonEvent["eventId"].GetValue<string>()),
EventType = (EventType)Enum.Parse(typeof(EventType), jsonEvent["eventType"].GetValue<string>()),
Timestamp = DateTime.Parse(jsonEvent["timestamp"].GetValue<string>()).ToUniversalTime(),
Timestamp = timestamp,
EntityId = Guid.Parse(jsonEvent["entityId"].GetValue<string>()),
EntityType = (EntityType)Enum.Parse(typeof(EntityType), jsonEvent["entityType"].GetValue<string>()),
EventData = JsonSerializer.Deserialize<DomainEvent>(newEventData, JsonSerializerOptions.Default),
Expand Down
3 changes: 3 additions & 0 deletions Application/Exceptions/AuthorizationException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace PlayOfferService.Application.Exceptions;

public class AuthorizationException(string message) : Exception(message);
38 changes: 27 additions & 11 deletions Application/Handlers/CancelPlayOfferHandler.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using MediatR;
using PlayOfferService.Application.Commands;
using PlayOfferService.Application.Exceptions;
using PlayOfferService.Domain;
using PlayOfferService.Domain.Events;
using PlayOfferService.Domain.Events.PlayOffer;
using PlayOfferService.Domain.Models;
Expand All @@ -11,40 +10,46 @@ namespace PlayOfferService.Application.Handlers;

public class CancelPlayOfferHandler : IRequestHandler<CancelPlayOfferCommand, Task>
{
private readonly DbWriteContext _context;
private readonly WriteEventRepository _writeEventRepository;
private readonly PlayOfferRepository _playOfferRepository;
private readonly ClubRepository _clubRepository;
public CancelPlayOfferHandler(DbWriteContext context, PlayOfferRepository playOfferRepository, ClubRepository clubRepository)

public CancelPlayOfferHandler(WriteEventRepository writeEventRepository, PlayOfferRepository playOfferRepository, ClubRepository clubRepository)
{
_context = context;
_writeEventRepository = writeEventRepository;
_playOfferRepository = playOfferRepository;
_clubRepository = clubRepository;
}

public async Task<Task> Handle(CancelPlayOfferCommand request, CancellationToken cancellationToken)
{
var transaction = _writeEventRepository.StartTransaction();
var excpectedEventCount = _writeEventRepository.GetEventCount(request.PlayOfferId) + 1;

var existingPlayOffer = (await _playOfferRepository.GetPlayOffersByIds(request.PlayOfferId)).FirstOrDefault();
if (existingPlayOffer == null)
throw new NotFoundException($"PlayOffer {request.PlayOfferId} not found!");
if (existingPlayOffer.CreatorId != request.MemberId)
throw new AuthorizationException($"PlayOffer {request.PlayOfferId} can only be cancelled by creator!");

if (existingPlayOffer.OpponentId != null)
throw new InvalidOperationException($"PlayOffer {request.PlayOfferId} is already accepted and cannot be cancelled!");
if (existingPlayOffer.IsCancelled)
throw new InvalidOperationException($"PlayOffer {request.PlayOfferId} is already cancelled!");

var existingClub = await _clubRepository.GetClubById(existingPlayOffer.ClubId);
if (existingClub == null)
throw new NotFoundException($"Club {existingPlayOffer.ClubId} not found!");


switch (existingClub.Status)
{
case Status.LOCKED:
throw new InvalidOperationException("Can't cancel PlayOffer while club is locked!");
case Status.DELETED:
throw new InvalidOperationException("Can't cancel PlayOffer in deleted club!");
}

var domainEvent = new BaseEvent
{
EntityId = request.PlayOfferId,
Expand All @@ -54,9 +59,20 @@ public async Task<Task> Handle(CancelPlayOfferCommand request, CancellationToken
EventData = new PlayOfferCancelledEvent(),
Timestamp = DateTime.UtcNow
};

await _writeEventRepository.AppendEvent(domainEvent);
await _writeEventRepository.Update();


var eventCount = _writeEventRepository.GetEventCount(request.PlayOfferId);

if (eventCount != excpectedEventCount)
{
transaction.Rollback();
throw new InvalidOperationException("Concurrent modification detected!");
}

_context.Events.Add(domainEvent);
await _context.SaveChangesAsync();
transaction.Commit();

return Task.CompletedTask;
}
Expand Down
Loading
Loading