Skip to content

Commit 127767a

Browse files
authored
Merge pull request #21 from THC-Software/dev
Dev (good luck)
2 parents fc6e1de + 4e871bc commit 127767a

File tree

105 files changed

+5509
-568
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

105 files changed

+5509
-568
lines changed

Application/Commands/CancelPlayOfferCommand.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22

33
namespace PlayOfferService.Application.Commands;
44

5-
public record CancelPlayOfferCommand(Guid PlayOfferId) : IRequest<Task>
5+
public record CancelPlayOfferCommand(Guid PlayOfferId, Guid MemberId) : IRequest<Task>
66
{
77
}

Application/Commands/CreatePlayOfferCommand.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22
using PlayOfferService.Domain.Models;
33

44
namespace PlayOfferService.Application.Commands;
5-
public record CreatePlayOfferCommand(PlayOfferDto PlayOfferDto) : IRequest<Guid>
5+
public record CreatePlayOfferCommand(CreatePlayOfferDto CreatePlayOfferDto, Guid CreatorId, Guid ClubId) : IRequest<Guid>
66
{
77
}

Application/Commands/JoinPlayOfferCommand.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22
using PlayOfferService.Domain.Models;
33

44
namespace PlayOfferService.Application.Commands;
5-
public record JoinPlayOfferCommand(JoinPlayOfferDto JoinPlayOfferDto) : IRequest<Task>
5+
public record JoinPlayOfferCommand(JoinPlayOfferDto JoinPlayOfferDto, Guid MemberId) : IRequest<Task>
66
{
77
}

Application/Controllers/PlayOfferController.cs

+103-18
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1+
using System.Security.Claims;
12
using MediatR;
3+
using Microsoft.AspNetCore.Authorization;
24
using Microsoft.AspNetCore.Mvc;
35
using PlayOfferService.Application.Commands;
6+
using PlayOfferService.Application.Exceptions;
47
using PlayOfferService.Application.Queries;
58
using PlayOfferService.Domain.Models;
69

710
namespace PlayOfferService.Application.Controllers;
811

912
[ApiController]
10-
[Route("api")]
13+
[Route("api/playoffers")]
1114
public class PlayOfferController : ControllerBase
1215
{
1316

@@ -18,49 +21,114 @@ public PlayOfferController(IMediator mediator)
1821
_mediator = mediator;
1922
}
2023

24+
/// <summary>
25+
/// Retrieve all Play Offers of the logged in users club
26+
/// </summary>
27+
/// <returns>Play offers with a matching club id</returns>
28+
/// <response code="200">Returns a list of Play offers matching the query params</response>
29+
/// <response code="204">No Play offer with matching properties was found</response>
30+
[HttpGet]
31+
[Authorize]
32+
[Route("club")]
33+
[ProducesResponseType(typeof(IEnumerable<PlayOfferDto>), StatusCodes.Status200OK)]
34+
[ProducesResponseType(typeof(ActionResult), StatusCodes.Status204NoContent)]
35+
[Consumes("application/json")]
36+
[Produces("application/json")]
37+
public async Task<ActionResult<IEnumerable<PlayOfferDto>>> GetByClubIdAsync()
38+
{
39+
var clubId = Guid.Parse(User.Claims.First(c => c.Type == "tennisClubId").Value);
40+
var result = await _mediator.Send(new GetPlayOffersByClubIdQuery(clubId));
41+
42+
if (result.Count() == 0)
43+
return NoContent();
44+
45+
return Ok(result);
46+
}
47+
2148
///<summary>
22-
///Retrieve all Play Offers matching the query params
49+
///Retrieve all Play Offers of a logged in user
2350
///</summary>
24-
///<param name="playOfferId">The id of the play offer</param>
25-
///<param name="creatorId">The id of the creator of the play offer</param>
26-
///<param name="clubId">The id of the club of the play offer</param>
27-
///<returns>Play offer with a matching id</returns>
28-
///<response code="200">Returns a Play offer matching the query params</response>
51+
///<returns>List of Play offers with where given member is creator or opponent</returns>
52+
///<response code="200">Returns a list of Play offers matching the query params</response>
2953
///<response code="204">No Play offer with matching properties was found</response>
3054
[HttpGet]
55+
[Authorize]
56+
[Route("participant")]
57+
[ProducesResponseType(typeof(IEnumerable<PlayOffer>), StatusCodes.Status200OK)]
58+
[ProducesResponseType(typeof(ActionResult), StatusCodes.Status204NoContent)]
59+
[Consumes("application/json")]
60+
[Produces("application/json")]
61+
public async Task<ActionResult<IEnumerable<PlayOfferDto>>> GetByParticipantIdAsync()
62+
{
63+
var participantId = Guid.Parse(User.FindFirst(ClaimTypes.NameIdentifier).Value);
64+
var result = await _mediator.Send(new GetPlayOffersByParticipantIdQuery(participantId));
65+
66+
if (result.Count() == 0)
67+
return NoContent();
68+
69+
return Ok(result);
70+
}
71+
72+
///<summary>
73+
///Get all Play offers created by a member with a matching name
74+
///</summary>
75+
///<param name="creatorName">Name of the creator in the format '[FirstName] [LastName]', '[FirstName]' or '[LastName]'</param>
76+
///<returns>A list of Play offers with a matching id</returns>
77+
///<response code="200">Returns a List of Play offers with creator matching the query params</response>
78+
///<response code="204">No Play offers with matching creator was found</response>
79+
[HttpGet]
80+
[Authorize]
81+
[Route("search")]
3182
[ProducesResponseType(typeof(IEnumerable<PlayOffer>), StatusCodes.Status200OK)]
3283
[ProducesResponseType(typeof(ActionResult), StatusCodes.Status204NoContent)]
3384
[Consumes("application/json")]
3485
[Produces("application/json")]
35-
public async Task<ActionResult<IEnumerable<PlayOffer>>> GetByIdAsync([FromQuery] Guid? playOfferId, [FromQuery] Guid? creatorId, [FromQuery] Guid? clubId)
86+
public async Task<ActionResult<IEnumerable<PlayOfferDto>>> GetByCreatorNameAsync([FromQuery] string creatorName)
3687
{
37-
var result = await _mediator.Send(new GetPlayOffersByIdQuery(playOfferId, creatorId, clubId));
88+
IEnumerable<PlayOfferDto> result;
89+
try
90+
{
91+
result = await _mediator.Send(new GetPlayOffersByCreatorNameQuery(creatorName));
92+
}
93+
catch (Exception e)
94+
{
95+
return BadRequest(e.Message);
96+
}
3897

3998
if (result.Count() == 0)
4099
return NoContent();
41100

42101
return Ok(result);
43102
}
103+
44104

45105

46106
///<summary>
47-
///Create a new Play Offer
107+
///Create a new Play Offer for the logged in user
48108
///</summary>
49-
///<param name="playOfferDto">The Play Offer to create</param>
109+
///<param name="createPlayOfferDto">The Play Offer to create</param>
50110
///<returns>The newly created Play offer</returns>
51111
///<response code="200">Returns the id of the created Play Offer</response>
52112
///<response code="400">Invalid Play Offer structure</response>
113+
///<response code="401">Only members can create Play Offers</response>
53114
[HttpPost]
115+
[Authorize]
54116
[ProducesResponseType(typeof(PlayOffer), StatusCodes.Status201Created)]
55117
[ProducesResponseType(typeof(ActionResult), StatusCodes.Status400BadRequest)]
56118
[Consumes("application/json")]
57119
[Produces("application/json")]
58-
public async Task<ActionResult<PlayOffer>> Create(PlayOfferDto playOfferDto)
120+
public async Task<ActionResult<PlayOffer>> Create(CreatePlayOfferDto createPlayOfferDto)
59121
{
122+
if (User.Claims.First(c => c.Type == "groups").Value != "MEMBER")
123+
return Unauthorized("Only members can create Play Offers!");
124+
125+
var creatorId = Guid.Parse(User.FindFirst(ClaimTypes.NameIdentifier)!.Value);
126+
var clubId = Guid.Parse(User.FindFirst("tennisClubId")!.Value);
127+
60128
Guid result;
61129
try
62130
{
63-
result = await _mediator.Send(new CreatePlayOfferCommand(playOfferDto));
131+
result = await _mediator.Send(new CreatePlayOfferCommand(createPlayOfferDto, creatorId, clubId));
64132
}
65133
catch (Exception e)
66134
{
@@ -71,22 +139,33 @@ public async Task<ActionResult<PlayOffer>> Create(PlayOfferDto playOfferDto)
71139
}
72140

73141
///<summary>
74-
///Cancels a Play Offer with a matching id
142+
///Cancels a Play Offer with a matching id of the logged in user
75143
///</summary>
76144
///<param name="playOfferId">The id of the Play Offer to cancel</param>
77145
///<returns>Nothing</returns>
78146
///<response code="200">The Play Offer with the matching id was cancelled</response>
79147
///<response code="400">No Play Offer with matching id found</response>
148+
///<response code="401">Only creator can cancel Play Offers</response>
80149
[HttpDelete]
150+
[Authorize]
81151
[ProducesResponseType(typeof(ActionResult), StatusCodes.Status200OK)]
82152
[ProducesResponseType(typeof(ActionResult), StatusCodes.Status400BadRequest)]
83153
[Consumes("application/json")]
84154
[Produces("application/json")]
85155
public async Task<ActionResult> Delete(Guid playOfferId)
86156
{
157+
if (User.Claims.First(c => c.Type == "groups").Value != "MEMBER")
158+
return Unauthorized("Only members can cancel Play Offers!");
159+
160+
var memberId = Guid.Parse(User.FindFirst(ClaimTypes.NameIdentifier)!.Value);
161+
87162
try
88163
{
89-
await _mediator.Send(new CancelPlayOfferCommand(playOfferId));
164+
await _mediator.Send(new CancelPlayOfferCommand(playOfferId, memberId));
165+
}
166+
catch (AuthorizationException e)
167+
{
168+
return Unauthorized(e.Message);
90169
}
91170
catch (Exception e)
92171
{
@@ -97,23 +176,29 @@ public async Task<ActionResult> Delete(Guid playOfferId)
97176
}
98177

99178
///<summary>
100-
///Adds a given opponentId to a Play Offer and creates a reservation
179+
///Logged in user joins a Play Offer with a matching playOfferId
101180
///</summary>
102181
///<param name="joinPlayOfferDto">The opponentId to add to the Play Offer with the matching playOfferId</param>
103182
///<returns>Nothing</returns>
104183
///<response code="200">The opponentId was added to the Play Offer with the matching playOfferId</response>
105184
///<response code="400">No playOffer with a matching playOfferId found</response>
185+
///<response code="401">Only members can join Play Offers</response>
106186
[HttpPost]
107-
[Route("/join")]
187+
[Authorize]
188+
[Route("join")]
108189
[ProducesResponseType(typeof(ActionResult), StatusCodes.Status200OK)]
109190
[ProducesResponseType(typeof(ActionResult), StatusCodes.Status400BadRequest)]
110191
[Consumes("application/json")]
111192
[Produces("application/json")]
112193
public async Task<ActionResult> Join(JoinPlayOfferDto joinPlayOfferDto)
113194
{
195+
if (User.Claims.First(c => c.Type == "groups").Value != "MEMBER")
196+
return Unauthorized("Only members can join Play Offers!");
197+
198+
var memberId = Guid.Parse(User.FindFirst(ClaimTypes.NameIdentifier)!.Value);
114199
try
115200
{
116-
await _mediator.Send(new JoinPlayOfferCommand(joinPlayOfferDto));
201+
await _mediator.Send(new JoinPlayOfferCommand(joinPlayOfferDto, memberId));
117202
}
118203
catch (Exception e)
119204
{

Application/EventParser.cs

+29-6
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,30 @@
11
using System.Text.Json;
22
using System.Text.Json.Nodes;
33
using PlayOfferService.Domain.Events;
4+
using PlayOfferService.Domain.Events.Reservation;
45

56
namespace PlayOfferService.Application;
67

78
public class EventParser
89
{
9-
public static BaseEvent ParseEvent(JsonNode jsonEvent)
10+
public static T ParseEvent<T>(JsonNode jsonEvent) where T : BaseEvent, new()
1011
{
11-
var originalEventData = JsonNode.Parse(jsonEvent["eventData"].GetValue<string>());
12+
JsonNode? originalEventData = null;
13+
DateTime timestamp = DateTime.UtcNow;
1214

15+
// If eventData is JsonValue (escaped as string), parse it
16+
if (jsonEvent["eventData"] is JsonValue)
17+
{
18+
originalEventData = JsonNode.Parse(jsonEvent["eventData"].GetValue<string>());
19+
timestamp = DateTime.Parse(jsonEvent["timestamp"].GetValue<string>()).ToUniversalTime();
20+
}
21+
else // If eventData is already a JsonNode, just use it --> court_service
22+
{
23+
originalEventData = jsonEvent["eventData"];
24+
timestamp = DateTimeOffset.FromUnixTimeMilliseconds(jsonEvent["timestamp"]["$date"].GetValue<long>())
25+
.UtcDateTime;
26+
}
27+
1328
// We need to add the eventType to the event data so we can deserialize it correctly
1429
// Since the discriminator needs to be at the first position in the JSON object, we need to create a new object
1530
// because JsonNode doesn't allow us to insert at the beginning of the object
@@ -18,14 +33,22 @@ public static BaseEvent ParseEvent(JsonNode jsonEvent)
1833
newEventData["eventType"] = jsonEvent["eventType"].GetValue<string>();
1934
foreach (var kvp in originalEventData.AsObject())
2035
{
21-
newEventData[kvp.Key] = kvp.Value.DeepClone();
36+
if (kvp.Value is JsonObject && kvp.Value?["$date"] != null)
37+
{
38+
newEventData[kvp.Key] = DateTimeOffset.FromUnixTimeMilliseconds(kvp.Value["$date"]
39+
.GetValue<long>()).UtcDateTime;
40+
}
41+
else
42+
{
43+
newEventData[kvp.Key] = kvp.Value.DeepClone();
44+
}
2245
}
23-
24-
return new BaseEvent
46+
47+
return new T
2548
{
2649
EventId = Guid.Parse(jsonEvent["eventId"].GetValue<string>()),
2750
EventType = (EventType)Enum.Parse(typeof(EventType), jsonEvent["eventType"].GetValue<string>()),
28-
Timestamp = DateTime.Parse(jsonEvent["timestamp"].GetValue<string>()).ToUniversalTime(),
51+
Timestamp = timestamp,
2952
EntityId = Guid.Parse(jsonEvent["entityId"].GetValue<string>()),
3053
EntityType = (EntityType)Enum.Parse(typeof(EntityType), jsonEvent["entityType"].GetValue<string>()),
3154
EventData = JsonSerializer.Deserialize<DomainEvent>(newEventData, JsonSerializerOptions.Default),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
namespace PlayOfferService.Application.Exceptions;
2+
3+
public class AuthorizationException(string message) : Exception(message);

Application/Handlers/CancelPlayOfferHandler.cs

+27-11
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using MediatR;
22
using PlayOfferService.Application.Commands;
33
using PlayOfferService.Application.Exceptions;
4-
using PlayOfferService.Domain;
54
using PlayOfferService.Domain.Events;
65
using PlayOfferService.Domain.Events.PlayOffer;
76
using PlayOfferService.Domain.Models;
@@ -11,40 +10,46 @@ namespace PlayOfferService.Application.Handlers;
1110

1211
public class CancelPlayOfferHandler : IRequestHandler<CancelPlayOfferCommand, Task>
1312
{
14-
private readonly DbWriteContext _context;
13+
private readonly WriteEventRepository _writeEventRepository;
1514
private readonly PlayOfferRepository _playOfferRepository;
1615
private readonly ClubRepository _clubRepository;
17-
18-
public CancelPlayOfferHandler(DbWriteContext context, PlayOfferRepository playOfferRepository, ClubRepository clubRepository)
16+
17+
public CancelPlayOfferHandler(WriteEventRepository writeEventRepository, PlayOfferRepository playOfferRepository, ClubRepository clubRepository)
1918
{
20-
_context = context;
19+
_writeEventRepository = writeEventRepository;
2120
_playOfferRepository = playOfferRepository;
2221
_clubRepository = clubRepository;
2322
}
2423

2524
public async Task<Task> Handle(CancelPlayOfferCommand request, CancellationToken cancellationToken)
2625
{
26+
var transaction = _writeEventRepository.StartTransaction();
27+
var excpectedEventCount = _writeEventRepository.GetEventCount(request.PlayOfferId) + 1;
28+
2729
var existingPlayOffer = (await _playOfferRepository.GetPlayOffersByIds(request.PlayOfferId)).FirstOrDefault();
2830
if (existingPlayOffer == null)
2931
throw new NotFoundException($"PlayOffer {request.PlayOfferId} not found!");
32+
if (existingPlayOffer.CreatorId != request.MemberId)
33+
throw new AuthorizationException($"PlayOffer {request.PlayOfferId} can only be cancelled by creator!");
34+
3035
if (existingPlayOffer.OpponentId != null)
3136
throw new InvalidOperationException($"PlayOffer {request.PlayOfferId} is already accepted and cannot be cancelled!");
3237
if (existingPlayOffer.IsCancelled)
3338
throw new InvalidOperationException($"PlayOffer {request.PlayOfferId} is already cancelled!");
34-
39+
3540
var existingClub = await _clubRepository.GetClubById(existingPlayOffer.ClubId);
3641
if (existingClub == null)
3742
throw new NotFoundException($"Club {existingPlayOffer.ClubId} not found!");
38-
39-
43+
44+
4045
switch (existingClub.Status)
4146
{
4247
case Status.LOCKED:
4348
throw new InvalidOperationException("Can't cancel PlayOffer while club is locked!");
4449
case Status.DELETED:
4550
throw new InvalidOperationException("Can't cancel PlayOffer in deleted club!");
4651
}
47-
52+
4853
var domainEvent = new BaseEvent
4954
{
5055
EntityId = request.PlayOfferId,
@@ -54,9 +59,20 @@ public async Task<Task> Handle(CancelPlayOfferCommand request, CancellationToken
5459
EventData = new PlayOfferCancelledEvent(),
5560
Timestamp = DateTime.UtcNow
5661
};
62+
63+
await _writeEventRepository.AppendEvent(domainEvent);
64+
await _writeEventRepository.Update();
65+
66+
67+
var eventCount = _writeEventRepository.GetEventCount(request.PlayOfferId);
68+
69+
if (eventCount != excpectedEventCount)
70+
{
71+
transaction.Rollback();
72+
throw new InvalidOperationException("Concurrent modification detected!");
73+
}
5774

58-
_context.Events.Add(domainEvent);
59-
await _context.SaveChangesAsync();
75+
transaction.Commit();
6076

6177
return Task.CompletedTask;
6278
}

0 commit comments

Comments
 (0)