From be2f07cdc56249fb5fd8ff12c98b10680e01d47a Mon Sep 17 00:00:00 2001 From: kubapoke Date: Sun, 27 Apr 2025 00:20:21 +0200 Subject: [PATCH 01/80] added files for shopping cart service --- TickAPI/TickAPI/Program.cs | 5 +++++ .../ShoppingCarts/Abstractions/IShoppingCartService.cs | 6 ++++++ TickAPI/TickAPI/ShoppingCarts/Models/ShoppingCart.cs | 8 ++++++++ .../TickAPI/ShoppingCarts/Services/ShoppingCartService.cs | 8 ++++++++ 4 files changed, 27 insertions(+) create mode 100644 TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs create mode 100644 TickAPI/TickAPI/ShoppingCarts/Models/ShoppingCart.cs create mode 100644 TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs diff --git a/TickAPI/TickAPI/Program.cs b/TickAPI/TickAPI/Program.cs index 59b7d54..f946f72 100644 --- a/TickAPI/TickAPI/Program.cs +++ b/TickAPI/TickAPI/Program.cs @@ -38,6 +38,8 @@ using TickAPI.Common.Claims.Services; using TickAPI.Common.Redis.Abstractions; using TickAPI.Common.Redis.Services; +using TickAPI.ShoppingCarts.Abstractions; +using TickAPI.ShoppingCarts.Services; // Builder constants const string allowClientPolicyName = "AllowClient"; @@ -112,6 +114,9 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); +// Add shopping cart services. +builder.Services.AddScoped(); + // Add common services. builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs new file mode 100644 index 0000000..80e4e6d --- /dev/null +++ b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs @@ -0,0 +1,6 @@ +namespace TickAPI.ShoppingCarts.Abstractions; + +public interface IShoppingCartService +{ + +} \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Models/ShoppingCart.cs b/TickAPI/TickAPI/ShoppingCarts/Models/ShoppingCart.cs new file mode 100644 index 0000000..6c39b9c --- /dev/null +++ b/TickAPI/TickAPI/ShoppingCarts/Models/ShoppingCart.cs @@ -0,0 +1,8 @@ +using TickAPI.Tickets.Models; + +namespace TickAPI.ShoppingCarts.Models; + +public class ShoppingCart +{ + public List Tickets { get; set; } +} \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs new file mode 100644 index 0000000..b1c9d15 --- /dev/null +++ b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs @@ -0,0 +1,8 @@ +using TickAPI.ShoppingCarts.Abstractions; + +namespace TickAPI.ShoppingCarts.Services; + +public class ShoppingCartService : IShoppingCartService +{ + +} \ No newline at end of file From 109be6078df181594f2ea18629317f457b2398ce Mon Sep 17 00:00:00 2001 From: kubapoke Date: Sun, 27 Apr 2025 02:03:47 +0200 Subject: [PATCH 02/80] added necessary endpoint mocks for the shopping carts controller --- .../Controllers/ShoppingCartsController.cs | 32 +++++++++++++++++++ .../ShoppingCarts/Models/ShoppingCart.cs | 2 +- .../Tickets/Models/ShoppingCartTicket.cs | 9 ++++++ 3 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs create mode 100644 TickAPI/TickAPI/Tickets/Models/ShoppingCartTicket.cs diff --git a/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs b/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs new file mode 100644 index 0000000..1651954 --- /dev/null +++ b/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Mvc; +using TickAPI.Common.Auth.Attributes; +using TickAPI.Common.Auth.Enums; +using TickAPI.ShoppingCarts.Abstractions; + +namespace TickAPI.ShoppingCarts.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class ShoppingCartsController : ControllerBase +{ + private readonly IShoppingCartService _shoppingCartService; + + public ShoppingCartsController(IShoppingCartService shoppingCartService) + { + _shoppingCartService = shoppingCartService; + } + + [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] + [HttpPost("add-ticket")] + public async Task AddTicket() + { + throw new NotImplementedException(); + } + + [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] + [HttpPost("checkout")] + public async Task Checkout() + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Models/ShoppingCart.cs b/TickAPI/TickAPI/ShoppingCarts/Models/ShoppingCart.cs index 6c39b9c..c734465 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Models/ShoppingCart.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Models/ShoppingCart.cs @@ -4,5 +4,5 @@ namespace TickAPI.ShoppingCarts.Models; public class ShoppingCart { - public List Tickets { get; set; } + public List Tickets { get; set; } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Models/ShoppingCartTicket.cs b/TickAPI/TickAPI/Tickets/Models/ShoppingCartTicket.cs new file mode 100644 index 0000000..c19904b --- /dev/null +++ b/TickAPI/TickAPI/Tickets/Models/ShoppingCartTicket.cs @@ -0,0 +1,9 @@ +namespace TickAPI.Tickets.Models; + +public class ShoppingCartTicket +{ + public Guid TicketTypeId { get; set; } + public Guid CustomerId { get; set; } + public string NameOnTicket { get; set; } + public string? Seats { get; set; } +} \ No newline at end of file From d13818015d1c3d290049243ef3060732f9819710 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Sun, 27 Apr 2025 02:08:07 +0200 Subject: [PATCH 03/80] added mocks for the rest of necessary endpoints --- .../Controllers/ShoppingCartsController.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs b/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs index 1651954..5c19622 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs @@ -17,12 +17,26 @@ public ShoppingCartsController(IShoppingCartService shoppingCartService) } [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] - [HttpPost("add-ticket")] + [HttpPost("ticket")] public async Task AddTicket() { throw new NotImplementedException(); } + [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] + [HttpGet("tickets")] + public async Task GetTickets() + { + throw new NotImplementedException(); + } + + [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] + [HttpDelete("ticket")] + public async Task DeleteTicket() + { + throw new NotImplementedException(); + } + [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] [HttpPost("checkout")] public async Task Checkout() From dd6041d7c6982fc91939190b84ffbdc29ab216d0 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Thu, 1 May 2025 11:20:23 +0200 Subject: [PATCH 04/80] minor fixes --- TickAPI/TickAPI/Common/Mail/Services/MailService.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/TickAPI/TickAPI/Common/Mail/Services/MailService.cs b/TickAPI/TickAPI/Common/Mail/Services/MailService.cs index 1e5a1c9..24456bb 100644 --- a/TickAPI/TickAPI/Common/Mail/Services/MailService.cs +++ b/TickAPI/TickAPI/Common/Mail/Services/MailService.cs @@ -1,5 +1,4 @@ -using System.Net; -using SendGrid; +using SendGrid; using SendGrid.Helpers.Mail; using TickAPI.Common.Mail.Abstractions; using TickAPI.Common.Mail.Models; @@ -9,8 +8,8 @@ namespace TickAPI.Common.Mail.Services; public class MailService : IMailService { - private SendGridClient _client; - private EmailAddress _fromEmailAddress; + private readonly SendGridClient _client; + private readonly EmailAddress _fromEmailAddress; public MailService(IConfiguration configuration) { From c275607c9f6e1f51ae05541e90ac042b329b7734 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Thu, 1 May 2025 12:44:44 +0200 Subject: [PATCH 05/80] Create `PaymentGatewayService` --- .../Abstractions/IPaymentGatewayService.cs | 9 +++++ .../Payment/Models/PaymentErrorResponsePG.cs | 7 ++++ .../Common/Payment/Models/PaymentRequestPG.cs | 11 ++++++ .../Payment/Models/PaymentResponsePG.cs | 8 ++++ .../Payment/Services/PaymentGatewayService.cs | 39 +++++++++++++++++++ TickAPI/TickAPI/Program.cs | 3 ++ TickAPI/TickAPI/appsettings.example.json | 4 +- 7 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 TickAPI/TickAPI/Common/Payment/Abstractions/IPaymentGatewayService.cs create mode 100644 TickAPI/TickAPI/Common/Payment/Models/PaymentErrorResponsePG.cs create mode 100644 TickAPI/TickAPI/Common/Payment/Models/PaymentRequestPG.cs create mode 100644 TickAPI/TickAPI/Common/Payment/Models/PaymentResponsePG.cs create mode 100644 TickAPI/TickAPI/Common/Payment/Services/PaymentGatewayService.cs diff --git a/TickAPI/TickAPI/Common/Payment/Abstractions/IPaymentGatewayService.cs b/TickAPI/TickAPI/Common/Payment/Abstractions/IPaymentGatewayService.cs new file mode 100644 index 0000000..5ba1be0 --- /dev/null +++ b/TickAPI/TickAPI/Common/Payment/Abstractions/IPaymentGatewayService.cs @@ -0,0 +1,9 @@ +using TickAPI.Common.Payment.Models; +using TickAPI.Common.Results.Generic; + +namespace TickAPI.Common.Payment.Abstractions; + +public interface IPaymentGatewayService +{ + Task> ProcessPayment(PaymentRequestPG request); +} diff --git a/TickAPI/TickAPI/Common/Payment/Models/PaymentErrorResponsePG.cs b/TickAPI/TickAPI/Common/Payment/Models/PaymentErrorResponsePG.cs new file mode 100644 index 0000000..74e1618 --- /dev/null +++ b/TickAPI/TickAPI/Common/Payment/Models/PaymentErrorResponsePG.cs @@ -0,0 +1,7 @@ +using System.Text.Json.Serialization; + +namespace TickAPI.Common.Payment.Models; + +public record PaymentErrorResponsePG( + [property: JsonPropertyName("error")] string Error +); diff --git a/TickAPI/TickAPI/Common/Payment/Models/PaymentRequestPG.cs b/TickAPI/TickAPI/Common/Payment/Models/PaymentRequestPG.cs new file mode 100644 index 0000000..40b551c --- /dev/null +++ b/TickAPI/TickAPI/Common/Payment/Models/PaymentRequestPG.cs @@ -0,0 +1,11 @@ +using System.Text.Json.Serialization; + +namespace TickAPI.Common.Payment.Models; + +public record PaymentRequestPG( + [property: JsonPropertyName("amount")] decimal Amount, + [property: JsonPropertyName("currency")] string Currency, + [property: JsonPropertyName("card_number")] string CardNumber, + [property: JsonPropertyName("card_expiry")] string CardExpiry, + [property: JsonPropertyName("cvv")] string CVV +); \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Payment/Models/PaymentResponsePG.cs b/TickAPI/TickAPI/Common/Payment/Models/PaymentResponsePG.cs new file mode 100644 index 0000000..fac4e2a --- /dev/null +++ b/TickAPI/TickAPI/Common/Payment/Models/PaymentResponsePG.cs @@ -0,0 +1,8 @@ +using System.Text.Json.Serialization; + +namespace TickAPI.Common.Payment.Models; + +public record PaymentResponsePG( + [property: JsonPropertyName("transaction_id")] string TransactionId, + [property: JsonPropertyName("status")] string Status +); diff --git a/TickAPI/TickAPI/Common/Payment/Services/PaymentGatewayService.cs b/TickAPI/TickAPI/Common/Payment/Services/PaymentGatewayService.cs new file mode 100644 index 0000000..e5a67f9 --- /dev/null +++ b/TickAPI/TickAPI/Common/Payment/Services/PaymentGatewayService.cs @@ -0,0 +1,39 @@ +using System.Text; +using System.Text.Json; +using TickAPI.Common.Payment.Abstractions; +using TickAPI.Common.Payment.Models; +using TickAPI.Common.Results.Generic; + +namespace TickAPI.Common.Payment.Services; + +public class PaymentGatewayService : IPaymentGatewayService +{ + private readonly IConfiguration _configuration; + private readonly IHttpClientFactory _httpClientFactory; + + public PaymentGatewayService(IConfiguration configuration, IHttpClientFactory httpClientFactory) + { + _configuration = configuration; + _httpClientFactory = httpClientFactory; + } + + public async Task> ProcessPayment(PaymentRequestPG request) + { + var client = _httpClientFactory.CreateClient(); + var url = _configuration["PaymentGateway:Url"]!; + var json = JsonSerializer.Serialize(request); + var content = new StringContent(json, Encoding.UTF8, "application/json"); + + var response = await client.PostAsync(url, content); + var jsonResponse = await response.Content.ReadAsStringAsync(); + + if (!response.IsSuccessStatusCode) + { + var errorResponse = JsonSerializer.Deserialize(jsonResponse, new JsonSerializerOptions()); + return Result.Failure((int)response.StatusCode, errorResponse!.Error); + } + + var successResponse = JsonSerializer.Deserialize(jsonResponse, new JsonSerializerOptions()); + return Result.Success(successResponse!); + } +} diff --git a/TickAPI/TickAPI/Program.cs b/TickAPI/TickAPI/Program.cs index f355ec8..cb5bd92 100644 --- a/TickAPI/TickAPI/Program.cs +++ b/TickAPI/TickAPI/Program.cs @@ -39,6 +39,8 @@ using TickAPI.Common.Redis.Services; using TickAPI.Common.Mail.Abstractions; using TickAPI.Common.Mail.Services; +using TickAPI.Common.Payment.Abstractions; +using TickAPI.Common.Payment.Services; // Builder constants const string allowClientPolicyName = "AllowClient"; @@ -122,6 +124,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); diff --git a/TickAPI/TickAPI/appsettings.example.json b/TickAPI/TickAPI/appsettings.example.json index bbb517d..420009a 100644 --- a/TickAPI/TickAPI/appsettings.example.json +++ b/TickAPI/TickAPI/appsettings.example.json @@ -26,10 +26,12 @@ "ExpirySeconds" : "3600" } }, - "SendGrid": { "ApiKey": "ApiKey", "FromEmail": "your_mail", "FromName": "Resellio" + }, + "PaymentGateway": { + "Url": "http://localhost:7474" } } \ No newline at end of file From 0ec7be2f71d6ad3694b8072044e6cc8e1f534d3e Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Fri, 2 May 2025 00:18:15 +0200 Subject: [PATCH 06/80] Fix invalid url --- .../Common/Payment/Services/PaymentGatewayService.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/TickAPI/TickAPI/Common/Payment/Services/PaymentGatewayService.cs b/TickAPI/TickAPI/Common/Payment/Services/PaymentGatewayService.cs index e5a67f9..f6f4a85 100644 --- a/TickAPI/TickAPI/Common/Payment/Services/PaymentGatewayService.cs +++ b/TickAPI/TickAPI/Common/Payment/Services/PaymentGatewayService.cs @@ -17,10 +17,16 @@ public PaymentGatewayService(IConfiguration configuration, IHttpClientFactory ht _httpClientFactory = httpClientFactory; } + public Task> HealthCheck() + { + throw new NotImplementedException(); + } + public async Task> ProcessPayment(PaymentRequestPG request) { var client = _httpClientFactory.CreateClient(); - var url = _configuration["PaymentGateway:Url"]!; + var baseUrl = _configuration["PaymentGateway:Url"]!; + var url = $"{baseUrl}/payments"; var json = JsonSerializer.Serialize(request); var content = new StringContent(json, Encoding.UTF8, "application/json"); From 51421d7baeb1a2dbf9ea25ebb6b853c552deca80 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Fri, 2 May 2025 00:18:27 +0200 Subject: [PATCH 07/80] Add missing field --- TickAPI/TickAPI/Common/Payment/Models/PaymentRequestPG.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TickAPI/TickAPI/Common/Payment/Models/PaymentRequestPG.cs b/TickAPI/TickAPI/Common/Payment/Models/PaymentRequestPG.cs index 40b551c..68511e8 100644 --- a/TickAPI/TickAPI/Common/Payment/Models/PaymentRequestPG.cs +++ b/TickAPI/TickAPI/Common/Payment/Models/PaymentRequestPG.cs @@ -7,5 +7,6 @@ public record PaymentRequestPG( [property: JsonPropertyName("currency")] string Currency, [property: JsonPropertyName("card_number")] string CardNumber, [property: JsonPropertyName("card_expiry")] string CardExpiry, - [property: JsonPropertyName("cvv")] string CVV + [property: JsonPropertyName("cvv")] string CVV, + [property: JsonPropertyName("force_error")] bool ForceError ); \ No newline at end of file From 57b62932b700b47ca0a8f1cc7826737ebfdbd2b6 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Fri, 2 May 2025 00:33:41 +0200 Subject: [PATCH 08/80] Create health check for payment gateway connection --- .../Abstractions/IPaymentGatewayService.cs | 1 + .../PaymentGatewayHealthStatusExtensions.cs | 11 ++++++++ .../Health/PaymentGatewayHealthCheck.cs | 25 +++++++++++++++++++ .../Models/PaymentGatewayHealthStatus.cs | 7 ++++++ .../Payment/Services/PaymentGatewayService.cs | 10 ++++++-- TickAPI/TickAPI/Program.cs | 5 +++- 6 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 TickAPI/TickAPI/Common/Payment/Extensions/PaymentGatewayHealthStatusExtensions.cs create mode 100644 TickAPI/TickAPI/Common/Payment/Health/PaymentGatewayHealthCheck.cs create mode 100644 TickAPI/TickAPI/Common/Payment/Models/PaymentGatewayHealthStatus.cs diff --git a/TickAPI/TickAPI/Common/Payment/Abstractions/IPaymentGatewayService.cs b/TickAPI/TickAPI/Common/Payment/Abstractions/IPaymentGatewayService.cs index 5ba1be0..b9ca162 100644 --- a/TickAPI/TickAPI/Common/Payment/Abstractions/IPaymentGatewayService.cs +++ b/TickAPI/TickAPI/Common/Payment/Abstractions/IPaymentGatewayService.cs @@ -5,5 +5,6 @@ namespace TickAPI.Common.Payment.Abstractions; public interface IPaymentGatewayService { + Task HealthCheck(); Task> ProcessPayment(PaymentRequestPG request); } diff --git a/TickAPI/TickAPI/Common/Payment/Extensions/PaymentGatewayHealthStatusExtensions.cs b/TickAPI/TickAPI/Common/Payment/Extensions/PaymentGatewayHealthStatusExtensions.cs new file mode 100644 index 0000000..629d4e0 --- /dev/null +++ b/TickAPI/TickAPI/Common/Payment/Extensions/PaymentGatewayHealthStatusExtensions.cs @@ -0,0 +1,11 @@ +using TickAPI.Common.Payment.Models; + +namespace TickAPI.Common.Payment.Extensions; + +public static class PaymentGatewayHealthStatusExtensions +{ + public static bool IsHealthy(this PaymentGatewayHealthStatus response) + { + return response.Status.Equals("ok", StringComparison.CurrentCultureIgnoreCase); + } +} diff --git a/TickAPI/TickAPI/Common/Payment/Health/PaymentGatewayHealthCheck.cs b/TickAPI/TickAPI/Common/Payment/Health/PaymentGatewayHealthCheck.cs new file mode 100644 index 0000000..858a9d3 --- /dev/null +++ b/TickAPI/TickAPI/Common/Payment/Health/PaymentGatewayHealthCheck.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.Diagnostics.HealthChecks; +using TickAPI.Common.Payment.Abstractions; +using TickAPI.Common.Payment.Extensions; + +namespace TickAPI.Common.Payment.Health; + +public class PaymentGatewayHealthCheck : IHealthCheck +{ + private readonly IPaymentGatewayService _paymentGateway; + + public PaymentGatewayHealthCheck(IPaymentGatewayService paymentGateway) + { + _paymentGateway = paymentGateway; + } + + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = new CancellationToken()) + { + var status = await _paymentGateway.HealthCheck(); + if (status.IsHealthy()) + { + return HealthCheckResult.Healthy("Payment gateway is reachable."); + } + return HealthCheckResult.Unhealthy("Payment gateway is not reachable."); + } +} diff --git a/TickAPI/TickAPI/Common/Payment/Models/PaymentGatewayHealthStatus.cs b/TickAPI/TickAPI/Common/Payment/Models/PaymentGatewayHealthStatus.cs new file mode 100644 index 0000000..e4b0b7a --- /dev/null +++ b/TickAPI/TickAPI/Common/Payment/Models/PaymentGatewayHealthStatus.cs @@ -0,0 +1,7 @@ +using System.Text.Json.Serialization; + +namespace TickAPI.Common.Payment.Models; + +public record PaymentGatewayHealthStatus( + [property: JsonPropertyName("status")] string Status +); diff --git a/TickAPI/TickAPI/Common/Payment/Services/PaymentGatewayService.cs b/TickAPI/TickAPI/Common/Payment/Services/PaymentGatewayService.cs index f6f4a85..f30f958 100644 --- a/TickAPI/TickAPI/Common/Payment/Services/PaymentGatewayService.cs +++ b/TickAPI/TickAPI/Common/Payment/Services/PaymentGatewayService.cs @@ -17,9 +17,15 @@ public PaymentGatewayService(IConfiguration configuration, IHttpClientFactory ht _httpClientFactory = httpClientFactory; } - public Task> HealthCheck() + public async Task HealthCheck() { - throw new NotImplementedException(); + var client = _httpClientFactory.CreateClient(); + var baseUrl = _configuration["PaymentGateway:Url"]!; + var url = $"{baseUrl}/health"; + var response = await client.GetAsync(url); + var jsonResponse = await response.Content.ReadAsStringAsync(); + var status = JsonSerializer.Deserialize(jsonResponse, new JsonSerializerOptions()); + return status!; } public async Task> ProcessPayment(PaymentRequestPG request) diff --git a/TickAPI/TickAPI/Program.cs b/TickAPI/TickAPI/Program.cs index cb5bd92..0a431a1 100644 --- a/TickAPI/TickAPI/Program.cs +++ b/TickAPI/TickAPI/Program.cs @@ -40,6 +40,7 @@ using TickAPI.Common.Mail.Abstractions; using TickAPI.Common.Mail.Services; using TickAPI.Common.Payment.Abstractions; +using TickAPI.Common.Payment.Health; using TickAPI.Common.Payment.Services; // Builder constants @@ -183,7 +184,9 @@ // TODO: when we start using redis we should probably also check here if we can connect to it // Setup healtcheck -builder.Services.AddHealthChecks().AddSqlServer(connectionString: builder.Configuration.GetConnectionString("ResellioDatabase") ?? ""); +builder.Services.AddHealthChecks() + .AddSqlServer(connectionString: builder.Configuration.GetConnectionString("ResellioDatabase") ?? "") + .AddCheck("PaymentGateway"); // Add http client builder.Services.AddHttpClient(); From 822960dd2e2a93a8150a552aecd5633805422996 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Fri, 2 May 2025 11:47:37 +0200 Subject: [PATCH 09/80] Add missing admin policies --- TickAPI/TickAPI/Categories/Controllers/CategoriesController.cs | 2 +- TickAPI/TickAPI/Organizers/Controllers/OrganizersController.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/TickAPI/TickAPI/Categories/Controllers/CategoriesController.cs b/TickAPI/TickAPI/Categories/Controllers/CategoriesController.cs index 684f0bc..8ac66a2 100644 --- a/TickAPI/TickAPI/Categories/Controllers/CategoriesController.cs +++ b/TickAPI/TickAPI/Categories/Controllers/CategoriesController.cs @@ -31,7 +31,7 @@ public async Task>> GetCatego return Ok(res.Value); } - // TODO: Add appropriate policy verification (admin, maybe also organizer?) + [AuthorizeWithPolicy(AuthPolicies.AdminPolicy)] [HttpPost] public async Task CreateCategory([FromBody] CreateCategoryDto request) { diff --git a/TickAPI/TickAPI/Organizers/Controllers/OrganizersController.cs b/TickAPI/TickAPI/Organizers/Controllers/OrganizersController.cs index cb405e3..f7f9a64 100644 --- a/TickAPI/TickAPI/Organizers/Controllers/OrganizersController.cs +++ b/TickAPI/TickAPI/Organizers/Controllers/OrganizersController.cs @@ -83,7 +83,7 @@ public async Task> CreateOrganizer([Fro return new ActionResult(new CreateOrganizerResponseDto(jwtTokenResult.Value!)); } - // TODO: Add authorization with admin policy here + [AuthorizeWithPolicy(AuthPolicies.AdminPolicy)] [HttpPost("verify")] public async Task VerifyOrganizer([FromBody] VerifyOrganizerDto request) { From 54cb5d2d41640779867090a13c09e09bd0816fcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Fri, 2 May 2025 11:57:56 +0200 Subject: [PATCH 10/80] Check if ticket is assigned to the customer and fetch the ticket --- .../Tickets/Abstractions/ITicketRepository.cs | 6 +++- .../Tickets/Controllers/TicketsController.cs | 10 +++++++ .../Response/GetTicketDetailsResponseDto.cs | 12 ++++++++ .../Tickets/Repositories/TicketRepository.cs | 29 ++++++++++++++++++- .../TickAPI/Tickets/Services/TicketService.cs | 17 +++++++++++ 5 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 TickAPI/TickAPI/Tickets/DTOs/Response/GetTicketDetailsResponseDto.cs diff --git a/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs b/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs index d3220d8..9edd2b3 100644 --- a/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs +++ b/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs @@ -1,4 +1,5 @@ -using TickAPI.Tickets.Models; +using TickAPI.Common.Results.Generic; +using TickAPI.Tickets.Models; using TickAPI.TicketTypes.Models; namespace TickAPI.Tickets.Abstractions; @@ -6,4 +7,7 @@ namespace TickAPI.Tickets.Abstractions; public interface ITicketRepository { public IQueryable GetAllTicketsByTicketType(TicketType ticketType); + public Task> CheckIfTicketBelongsToCustomerAsync(Guid id, string email); + + public Task> GetTicketByIdAsync(Guid id); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs b/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs index fcf4467..b3ac856 100644 --- a/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs +++ b/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs @@ -1,4 +1,6 @@ using Microsoft.AspNetCore.Mvc; +using TickAPI.Common.Auth.Attributes; +using TickAPI.Common.Auth.Enums; namespace TickAPI.Tickets.Controllers; @@ -6,5 +8,13 @@ namespace TickAPI.Tickets.Controllers; [Route("api/[controller]")] public class TicketsController : ControllerBase { + public TicketsController() + { + + } + + // [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] + // [HttpGet("{id:guid}")] + } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/DTOs/Response/GetTicketDetailsResponseDto.cs b/TickAPI/TickAPI/Tickets/DTOs/Response/GetTicketDetailsResponseDto.cs new file mode 100644 index 0000000..d0ce813 --- /dev/null +++ b/TickAPI/TickAPI/Tickets/DTOs/Response/GetTicketDetailsResponseDto.cs @@ -0,0 +1,12 @@ +namespace TickAPI.Tickets.DTOs.Response; + +public class GetTicketDetailsResponseDto +{ + public string NameOnTicket { get; set; } + public string? Seats { get; set; } + public bool ForResell { get; set; } + public string EventName {get; set;} + public string OrganizerName {get; set;} + public DateTime StartDate { get; set; } + public DateTime EndDate { get; set; } +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs b/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs index 42ade22..df6dd82 100644 --- a/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs +++ b/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs @@ -1,4 +1,7 @@ -using TickAPI.Common.TickApiDbContext; +using Microsoft.EntityFrameworkCore; +using TickAPI.Common.Results; +using TickAPI.Common.Results.Generic; +using TickAPI.Common.TickApiDbContext; using TickAPI.Tickets.Abstractions; using TickAPI.Tickets.Models; using TickAPI.TicketTypes.Models; @@ -18,4 +21,28 @@ public IQueryable GetAllTicketsByTicketType(TicketType ticketType) { return _tickApiDbContext.Tickets.Where(t => t.Type == ticketType); } + + public async Task> CheckIfTicketBelongsToCustomerAsync(Guid id, string email) + { + var count = await _tickApiDbContext.Tickets.Join(_tickApiDbContext.Customers, + ticket => ticket.Owner.Id, customer => customer.Id, (ticket, customer) => new {ticket.Id}).CountAsync(); + + if (count > 0) + { + return Result.Success(true); + } + + return Result.Failure(StatusCodes.Status404NotFound, "Ticket with this id doesn't exist " + + "for this user"); + } + + public async Task> GetTicketByIdAsync(Guid id) + { + var ticket = await _tickApiDbContext.Tickets.FindAsync(id); + if (ticket == null) + { + return Result.Failure(StatusCodes.Status404NotFound, "Ticket with this id doesn't exist"); + } + return Result.Success(ticket); + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Services/TicketService.cs b/TickAPI/TickAPI/Tickets/Services/TicketService.cs index d8d7855..9e0264f 100644 --- a/TickAPI/TickAPI/Tickets/Services/TicketService.cs +++ b/TickAPI/TickAPI/Tickets/Services/TicketService.cs @@ -1,5 +1,6 @@ using TickAPI.Common.Results.Generic; using TickAPI.Tickets.Abstractions; +using TickAPI.Tickets.DTOs.Response; using TickAPI.TicketTypes.Models; namespace TickAPI.Tickets.Services; @@ -28,4 +29,20 @@ public Result GetNumberOfAvailableTicketsByType(TicketType ticketType) return Result.Success((uint)availableCount); } + + public async Task> GetTicketDetailsAsync(string email, Guid ticketGuid) + { + var exists = await _ticketRepository.CheckIfTicketBelongsToCustomerAsync(ticketGuid, email); + if (!exists.IsSuccess) + { + return Result.PropagateError(exists); + } + + var ticket = await _ticketRepository.GetTicketByIdAsync(ticketGuid); + if (!ticket.IsSuccess) + { + return Result.PropagateError(ticket); + } + + } } \ No newline at end of file From b2a69618dec2ed9ad1ed6ce372fe9798f7b0842d Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Fri, 2 May 2025 12:20:55 +0200 Subject: [PATCH 11/80] Implement getting tickets for resell for given event --- .../Tickets/Abstractions/ITicketRepository.cs | 1 + .../Tickets/Abstractions/ITicketService.cs | 9 +++++--- .../Tickets/Controllers/TicketsController.cs | 19 +++++++++++++++ .../Response/GetTicketForResellResponseDto.cs | 9 ++++++++ .../Tickets/Repositories/TicketRepository.cs | 11 ++++++++- .../TickAPI/Tickets/Services/TicketService.cs | 23 +++++++++++++++++-- 6 files changed, 66 insertions(+), 6 deletions(-) create mode 100644 TickAPI/TickAPI/Tickets/DTOs/Response/GetTicketForResellResponseDto.cs diff --git a/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs b/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs index d3220d8..8ec26d6 100644 --- a/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs +++ b/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs @@ -6,4 +6,5 @@ namespace TickAPI.Tickets.Abstractions; public interface ITicketRepository { public IQueryable GetAllTicketsByTicketType(TicketType ticketType); + public IQueryable GetTicketsByEventId(Guid eventId); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs b/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs index 85e9ac5..e6d0d55 100644 --- a/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs +++ b/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs @@ -1,9 +1,12 @@ -using TickAPI.Common.Results.Generic; +using TickAPI.Common.Pagination.Responses; +using TickAPI.Common.Results.Generic; +using TickAPI.Tickets.DTOs.Response; using TickAPI.TicketTypes.Models; namespace TickAPI.Tickets.Abstractions; public interface ITicketService -{ - public Result GetNumberOfAvailableTicketsByType(TicketType ticketType); +{ + Result GetNumberOfAvailableTicketsByType(TicketType ticketType); + Task>> GetTicketsForResellAsync(Guid eventId, int page, int pageSize); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs b/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs index fcf4467..005ac16 100644 --- a/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs +++ b/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs @@ -1,4 +1,7 @@ using Microsoft.AspNetCore.Mvc; +using TickAPI.Common.Pagination.Responses; +using TickAPI.Tickets.Abstractions; +using TickAPI.Tickets.DTOs.Response; namespace TickAPI.Tickets.Controllers; @@ -6,5 +9,21 @@ namespace TickAPI.Tickets.Controllers; [Route("api/[controller]")] public class TicketsController : ControllerBase { + private readonly ITicketService _ticketService; + + public TicketsController(ITicketService ticketService) + { + _ticketService = ticketService; + } + [HttpGet("/for-resell")] + public async Task>> GetTicketsForResell([FromQuery] Guid eventId, [FromQuery] int pageSize, [FromQuery] int page) + { + var result = await _ticketService.GetTicketsForResellAsync(eventId, page, pageSize); + if (result.IsError) + { + return StatusCode(result.StatusCode, result.ErrorMsg); + } + return result.Value!; + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/DTOs/Response/GetTicketForResellResponseDto.cs b/TickAPI/TickAPI/Tickets/DTOs/Response/GetTicketForResellResponseDto.cs new file mode 100644 index 0000000..831ad47 --- /dev/null +++ b/TickAPI/TickAPI/Tickets/DTOs/Response/GetTicketForResellResponseDto.cs @@ -0,0 +1,9 @@ +namespace TickAPI.Tickets.DTOs.Response; + +public record GetTicketForResellResponseDto( + Guid Id, + decimal Price, + string Currency, + string Description, + string? Seats +); diff --git a/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs b/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs index 42ade22..c85566d 100644 --- a/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs +++ b/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs @@ -1,4 +1,5 @@ -using TickAPI.Common.TickApiDbContext; +using Microsoft.EntityFrameworkCore; +using TickAPI.Common.TickApiDbContext; using TickAPI.Tickets.Abstractions; using TickAPI.Tickets.Models; using TickAPI.TicketTypes.Models; @@ -18,4 +19,12 @@ public IQueryable GetAllTicketsByTicketType(TicketType ticketType) { return _tickApiDbContext.Tickets.Where(t => t.Type == ticketType); } + + public IQueryable GetTicketsByEventId(Guid eventId) + { + return _tickApiDbContext.Tickets + .Include(t => t.Type) + .Include(t => t.Type.Event) + .Where(t => t.Type.Event.Id == eventId); + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Services/TicketService.cs b/TickAPI/TickAPI/Tickets/Services/TicketService.cs index d8d7855..4b5e7b6 100644 --- a/TickAPI/TickAPI/Tickets/Services/TicketService.cs +++ b/TickAPI/TickAPI/Tickets/Services/TicketService.cs @@ -1,5 +1,8 @@ -using TickAPI.Common.Results.Generic; +using TickAPI.Common.Pagination.Abstractions; +using TickAPI.Common.Pagination.Responses; +using TickAPI.Common.Results.Generic; using TickAPI.Tickets.Abstractions; +using TickAPI.Tickets.DTOs.Response; using TickAPI.TicketTypes.Models; namespace TickAPI.Tickets.Services; @@ -7,10 +10,12 @@ namespace TickAPI.Tickets.Services; public class TicketService : ITicketService { private readonly ITicketRepository _ticketRepository; + private readonly IPaginationService _paginationService; - public TicketService(ITicketRepository ticketRepository) + public TicketService(ITicketRepository ticketRepository, IPaginationService paginationService) { _ticketRepository = ticketRepository; + _paginationService = paginationService; } // TODO: Update this method to also count tickets cached in Redis as unavailable @@ -28,4 +33,18 @@ public Result GetNumberOfAvailableTicketsByType(TicketType ticketType) return Result.Success((uint)availableCount); } + + public async Task>> GetTicketsForResellAsync(Guid eventId, int page, int pageSize) + { + var eventTickets = _ticketRepository.GetTicketsByEventId(eventId); + var ticketsForResell = eventTickets.Where(t => t.ForResell); + var paginatedTicketsResult = await _paginationService.PaginateAsync(ticketsForResell, pageSize, page); + if (paginatedTicketsResult.IsError) + { + return Result>.PropagateError(paginatedTicketsResult); + } + var paginatedResult = _paginationService.MapData(paginatedTicketsResult.Value!, + t => new GetTicketForResellResponseDto(t.Id, t.Type.Price, t.Type.Currency, t.Type.Description, t.Seats)); + return Result>.Success(paginatedResult); + } } \ No newline at end of file From 8210f95ddfb991786e30bca785d554daff33ffed Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Fri, 2 May 2025 12:22:26 +0200 Subject: [PATCH 12/80] Fix tests --- .../Tickets/Services/TicketServiceTests.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs b/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs index 068a29f..5b7eda0 100644 --- a/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Http; using Moq; +using TickAPI.Common.Pagination.Abstractions; using TickAPI.Common.Results.Generic; using TickAPI.Tickets.Abstractions; using TickAPI.Tickets.Models; @@ -17,13 +18,14 @@ public void GetNumberOfAvailableTicketsByType_AmountsAreCorrect_ShouldReturnCorr var type = new TicketType { MaxCount = 30 }; var ticketList = new List(new Ticket[10]); - Mock ticketRepositoryMock = new Mock(); + var ticketRepositoryMock = new Mock(); + var paginationServiceMock = new Mock(); ticketRepositoryMock .Setup(m => m.GetAllTicketsByTicketType(type)) .Returns(ticketList.AsQueryable()); - var sut = new TicketService(ticketRepositoryMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object); // Act var result = sut.GetNumberOfAvailableTicketsByType(type); @@ -40,13 +42,14 @@ public void GetNumberOfAvailableTicketsByType_WhenMoreTicketExistThanMaxCount_Sh var type = new TicketType { MaxCount = 30 }; var ticketList = new List(new Ticket[50]); - Mock ticketRepositoryMock = new Mock(); + var ticketRepositoryMock = new Mock(); + var paginationServiceMock = new Mock(); ticketRepositoryMock .Setup(m => m.GetAllTicketsByTicketType(type)) .Returns(ticketList.AsQueryable()); - var sut = new TicketService(ticketRepositoryMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object); // Act var result = sut.GetNumberOfAvailableTicketsByType(type); From 50fc33335b57f89867498967daf41cf9d2c66f17 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Fri, 2 May 2025 12:39:36 +0200 Subject: [PATCH 13/80] Add tests --- .../Tickets/Services/TicketServiceTests.cs | 267 ++++++++++++++++++ 1 file changed, 267 insertions(+) diff --git a/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs b/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs index 5b7eda0..dd1a7bf 100644 --- a/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs @@ -1,8 +1,10 @@ using Microsoft.AspNetCore.Http; using Moq; using TickAPI.Common.Pagination.Abstractions; +using TickAPI.Common.Pagination.Responses; using TickAPI.Common.Results.Generic; using TickAPI.Tickets.Abstractions; +using TickAPI.Tickets.DTOs.Response; using TickAPI.Tickets.Models; using TickAPI.Tickets.Services; using TickAPI.TicketTypes.Models; @@ -59,4 +61,269 @@ public void GetNumberOfAvailableTicketsByType_WhenMoreTicketExistThanMaxCount_Sh Assert.Equal(StatusCodes.Status500InternalServerError, result.StatusCode); Assert.Equal("The number of available tickets is negative.", result.ErrorMsg); } + + [Fact] + public async Task GetTicketsForResellAsync_WhenDataIsValid_ShouldReturnSuccess() + { + // Arrange + var eventId = Guid.NewGuid(); + int pageSize = 10; + int page = 0; + + var ticket1 = new Ticket + { + Id = Guid.NewGuid(), + ForResell = true, + Type = new TicketType + { + Price = 50m, + Currency = "USD", + Description = "VIP" + }, + Seats = "A1" + }; + + var ticket2 = new Ticket + { + Id = Guid.NewGuid(), + ForResell = true, + Type = new TicketType + { + Price = 30m, + Currency = "USD", + Description = "Standard" + }, + Seats = "B2" + }; + + var allTickets = new List { ticket1, ticket2 }.AsQueryable(); + + var ticketRepositoryMock = new Mock(); + ticketRepositoryMock.Setup(repo => repo.GetTicketsByEventId(eventId)) + .Returns(allTickets); + + var paginatedTickets = new PaginatedData( + new List { ticket1, ticket2 }, + page, + pageSize, + false, + false, + new PaginationDetails(0, 2) + ); + + var paginationServiceMock = new Mock(); + paginationServiceMock.Setup(p => p.PaginateAsync(It.IsAny>(), pageSize, page)) + .ReturnsAsync(Result>.Success(paginatedTickets)); + + var mappedDto1 = new GetTicketForResellResponseDto( + ticket1.Id, + ticket1.Type.Price, + ticket1.Type.Currency, + ticket1.Type.Description, + ticket1.Seats + ); + + var mappedDto2 = new GetTicketForResellResponseDto( + ticket2.Id, + ticket2.Type.Price, + ticket2.Type.Currency, + ticket2.Type.Description, + ticket2.Seats + ); + + var mappedData = new PaginatedData( + new List { mappedDto1, mappedDto2 }, + page, + pageSize, + false, + false, + new PaginationDetails(0, 2) + ); + + paginationServiceMock.Setup(p => p.MapData( + paginatedTickets, + It.IsAny>())) + .Returns(mappedData); + + var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object); + + // Act + var result = await sut.GetTicketsForResellAsync(eventId, page, pageSize); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(2, result.Value!.Data.Count); + Assert.Equal(mappedDto1, result.Value!.Data[0]); + Assert.Equal(mappedDto2, result.Value!.Data[1]); + } + + [Fact] + public async Task GetTicketsForResellAsync_WhenNoTicketsForResell_ShouldReturnEmptyList() + { + // Arrange + var eventId = Guid.NewGuid(); + int pageSize = 10; + int page = 0; + + var tickets = new List + { + new Ticket + { + Id = Guid.NewGuid(), + ForResell = false, + Type = new TicketType + { + Price = 50m, + Currency = "USD", + Description = "VIP" + } + }, + new Ticket + { + Id = Guid.NewGuid(), + ForResell = false, + Type = new TicketType + { + Price = 30m, + Currency = "USD", + Description = "Standard" + } + } + }.AsQueryable(); + + var ticketRepositoryMock = new Mock(); + ticketRepositoryMock.Setup(repo => repo.GetTicketsByEventId(eventId)) + .Returns(tickets); + + var paginatedData = new PaginatedData( + new List(), + page, + pageSize, + false, + false, + new PaginationDetails(0, 0) + ); + + var paginationServiceMock = new Mock(); + paginationServiceMock.Setup(p => p.PaginateAsync(It.IsAny>(), pageSize, page)) + .ReturnsAsync(Result>.Success(paginatedData)); + + var mappedData = new PaginatedData( + new List(), + page, + pageSize, + false, + false, + new PaginationDetails(0, 0) + ); + + paginationServiceMock.Setup(p => p.MapData( + paginatedData, + It.IsAny>())) + .Returns(mappedData); + + var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object); + + // Act + var result = await sut.GetTicketsForResellAsync(eventId, page, pageSize); + + // Assert + Assert.True(result.IsSuccess); + Assert.Empty(result.Value!.Data); + } + + [Fact] + public async Task GetTicketsForResellAsync_WhenPaginationFails_ShouldPropagateError() + { + // Arrange + var eventId = Guid.NewGuid(); + int pageSize = 10; + int page = 0; + const string errorMsg = "Invalid pagination parameters"; + const int statusCode = 400; + + var tickets = new List + { + new Ticket + { + Id = Guid.NewGuid(), + ForResell = true, + Type = new TicketType + { + Price = 50m, + Currency = "USD", + Description = "VIP" + } + } + }.AsQueryable(); + + var ticketRepositoryMock = new Mock(); + ticketRepositoryMock.Setup(repo => repo.GetTicketsByEventId(eventId)) + .Returns(tickets); + + var paginationServiceMock = new Mock(); + paginationServiceMock.Setup(p => p.PaginateAsync(It.IsAny>(), pageSize, page)) + .ReturnsAsync(Result>.Failure(statusCode, errorMsg)); + + var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object); + + // Act + var result = await sut.GetTicketsForResellAsync(eventId, page, pageSize); + + // Assert + Assert.True(result.IsError); + Assert.Equal(errorMsg, result.ErrorMsg); + Assert.Equal(statusCode, result.StatusCode); + } + + [Fact] + public async Task GetTicketsForResellAsync_WhenNoTicketsForEvent_ShouldReturnEmptyList() + { + // Arrange + var eventId = Guid.NewGuid(); + int pageSize = 10; + int page = 0; + + var tickets = new List().AsQueryable(); + + var ticketRepositoryMock = new Mock(); + ticketRepositoryMock.Setup(repo => repo.GetTicketsByEventId(eventId)) + .Returns(tickets); + + var paginatedData = new PaginatedData( + new List(), + page, + pageSize, + false, + false, + new PaginationDetails(0, 0) + ); + + var paginationServiceMock = new Mock(); + paginationServiceMock.Setup(p => p.PaginateAsync(It.IsAny>(), pageSize, page)) + .ReturnsAsync(Result>.Success(paginatedData)); + + var mappedData = new PaginatedData( + new List(), + page, + pageSize, + false, + false, + new PaginationDetails(0, 0) + ); + + paginationServiceMock.Setup(p => p.MapData( + paginatedData, + It.IsAny>())) + .Returns(mappedData); + + var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object); + + // Act + var result = await sut.GetTicketsForResellAsync(eventId, page, pageSize); + + // Assert + Assert.True(result.IsSuccess); + Assert.Empty(result.Value!.Data); + } } \ No newline at end of file From 687bd13a0c2a52b43819ea912a7694176f7d4622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Fri, 2 May 2025 15:16:30 +0200 Subject: [PATCH 14/80] Working endpoint --- .../Events/Repositories/EventRepository.cs | 4 ++- .../TickAPI/Events/Services/EventService.cs | 1 + .../Tickets/Abstractions/ITicketService.cs | 2 ++ .../Tickets/Controllers/TicketsController.cs | 32 +++++++++++++++---- .../Response/GetTicketDetailsResponseDto.cs | 3 +- .../Tickets/Repositories/TicketRepository.cs | 3 +- .../TickAPI/Tickets/Services/TicketService.cs | 21 ++++++++++-- 7 files changed, 54 insertions(+), 12 deletions(-) diff --git a/TickAPI/TickAPI/Events/Repositories/EventRepository.cs b/TickAPI/TickAPI/Events/Repositories/EventRepository.cs index 391b613..839c130 100644 --- a/TickAPI/TickAPI/Events/Repositories/EventRepository.cs +++ b/TickAPI/TickAPI/Events/Repositories/EventRepository.cs @@ -1,4 +1,5 @@ -using TickAPI.Common.Results.Generic; +using Microsoft.EntityFrameworkCore; +using TickAPI.Common.Results.Generic; using TickAPI.Common.TickApiDbContext; using TickAPI.Events.Abstractions; using TickAPI.Events.Models; @@ -42,4 +43,5 @@ public Result GetEventById(Guid eventId) return Result.Success(@event); } + } \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/Services/EventService.cs b/TickAPI/TickAPI/Events/Services/EventService.cs index 92cbb80..5fedcd6 100644 --- a/TickAPI/TickAPI/Events/Services/EventService.cs +++ b/TickAPI/TickAPI/Events/Services/EventService.cs @@ -183,4 +183,5 @@ private static GetEventResponseDto MapEventToGetEventResponseDto(Event ev) return new GetEventResponseDto(ev.Id, ev.Name, ev.Description, ev.StartDate, ev.EndDate, ev.MinimumAge, minimumPrice, maximumPrice, categories, ev.EventStatus, address); } + } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs b/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs index 85e9ac5..85eb72f 100644 --- a/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs +++ b/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs @@ -1,4 +1,5 @@ using TickAPI.Common.Results.Generic; +using TickAPI.Tickets.DTOs.Response; using TickAPI.TicketTypes.Models; namespace TickAPI.Tickets.Abstractions; @@ -6,4 +7,5 @@ namespace TickAPI.Tickets.Abstractions; public interface ITicketService { public Result GetNumberOfAvailableTicketsByType(TicketType ticketType); + public Task> GetTicketDetailsAsync(string email, Guid ticketGuid); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs b/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs index b3ac856..c0e2998 100644 --- a/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs +++ b/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs @@ -1,6 +1,9 @@ using Microsoft.AspNetCore.Mvc; using TickAPI.Common.Auth.Attributes; using TickAPI.Common.Auth.Enums; +using TickAPI.Common.Claims.Abstractions; +using TickAPI.Tickets.Abstractions; +using TickAPI.Tickets.DTOs.Response; namespace TickAPI.Tickets.Controllers; @@ -8,13 +11,30 @@ namespace TickAPI.Tickets.Controllers; [Route("api/[controller]")] public class TicketsController : ControllerBase { - public TicketsController() + private readonly IClaimsService _claimsService; + private readonly ITicketService _ticketService; + public TicketsController(IClaimsService claimsService, ITicketService ticketService) { - + _claimsService = claimsService; + _ticketService = ticketService; + } + + [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] + [HttpGet("{id:guid}")] + public async Task> GetTicketDetails(Guid id) + { + var emailResult = _claimsService.GetEmailFromClaims(User.Claims); + if (emailResult.IsError) + { + return StatusCode(emailResult.StatusCode, emailResult.ErrorMsg); + } + var email = emailResult.Value!; + var ticket = await _ticketService.GetTicketDetailsAsync(email, id); + if (!ticket.IsSuccess) + { + return StatusCode(ticket.StatusCode, ticket.ErrorMsg); + } + return Ok(ticket.Value); } - - // [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] - // [HttpGet("{id:guid}")] - } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/DTOs/Response/GetTicketDetailsResponseDto.cs b/TickAPI/TickAPI/Tickets/DTOs/Response/GetTicketDetailsResponseDto.cs index d0ce813..a0c6423 100644 --- a/TickAPI/TickAPI/Tickets/DTOs/Response/GetTicketDetailsResponseDto.cs +++ b/TickAPI/TickAPI/Tickets/DTOs/Response/GetTicketDetailsResponseDto.cs @@ -4,7 +4,8 @@ public class GetTicketDetailsResponseDto { public string NameOnTicket { get; set; } public string? Seats { get; set; } - public bool ForResell { get; set; } + public decimal Price { get; set; } + public string Currency { get; set; } public string EventName {get; set;} public string OrganizerName {get; set;} public DateTime StartDate { get; set; } diff --git a/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs b/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs index df6dd82..89d0bf5 100644 --- a/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs +++ b/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs @@ -38,7 +38,8 @@ public async Task> CheckIfTicketBelongsToCustomerAsync(Guid id, str public async Task> GetTicketByIdAsync(Guid id) { - var ticket = await _tickApiDbContext.Tickets.FindAsync(id); + var ticket = await _tickApiDbContext.Tickets.Include(t => t.Type).Include(t => t.Type.Event) + .Include(t => t.Type.Event.Organizer).FirstOrDefaultAsync(); if (ticket == null) { return Result.Failure(StatusCodes.Status404NotFound, "Ticket with this id doesn't exist"); diff --git a/TickAPI/TickAPI/Tickets/Services/TicketService.cs b/TickAPI/TickAPI/Tickets/Services/TicketService.cs index 9e0264f..3cd03fc 100644 --- a/TickAPI/TickAPI/Tickets/Services/TicketService.cs +++ b/TickAPI/TickAPI/Tickets/Services/TicketService.cs @@ -3,6 +3,7 @@ using TickAPI.Tickets.DTOs.Response; using TickAPI.TicketTypes.Models; + namespace TickAPI.Tickets.Services; public class TicketService : ITicketService @@ -38,11 +39,25 @@ public async Task> GetTicketDetailsAsync(str return Result.PropagateError(exists); } - var ticket = await _ticketRepository.GetTicketByIdAsync(ticketGuid); - if (!ticket.IsSuccess) + var ticketRes = await _ticketRepository.GetTicketByIdAsync(ticketGuid); + if (!ticketRes.IsSuccess) { - return Result.PropagateError(ticket); + return Result.PropagateError(ticketRes); } + var ticket = ticketRes.Value; + var ticketDetails = new GetTicketDetailsResponseDto + { + NameOnTicket = ticket.NameOnTicket, + Seats = ticket.Seats, + Price = ticket.Type.Price, + Currency = ticket.Type.Currency, + EventName = ticket.Type.Event.Name, + OrganizerName = ticket.Type.Event.Organizer.DisplayName, + StartDate = ticket.Type.Event.StartDate, + EndDate = ticket.Type.Event.EndDate, + }; + return Result.Success(ticketDetails); + } } \ No newline at end of file From fc28f01c70f2770fb760c8df31b64ac18c12f6df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Fri, 2 May 2025 15:33:24 +0200 Subject: [PATCH 15/80] Add address --- .../DTOs/Response/GetTicketDetailsAddressDto.cs | 10 ++++++++++ .../DTOs/Response/GetTicketDetailsResponseDto.cs | 6 +++++- .../TickAPI/Tickets/Repositories/TicketRepository.cs | 3 ++- TickAPI/TickAPI/Tickets/Services/TicketService.cs | 5 +++++ 4 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 TickAPI/TickAPI/Tickets/DTOs/Response/GetTicketDetailsAddressDto.cs diff --git a/TickAPI/TickAPI/Tickets/DTOs/Response/GetTicketDetailsAddressDto.cs b/TickAPI/TickAPI/Tickets/DTOs/Response/GetTicketDetailsAddressDto.cs new file mode 100644 index 0000000..c6dd05c --- /dev/null +++ b/TickAPI/TickAPI/Tickets/DTOs/Response/GetTicketDetailsAddressDto.cs @@ -0,0 +1,10 @@ +namespace TickAPI.Tickets.DTOs.Response; + +public record GetTicketDetailsAddressDto( + string Country, + string City, + string PostalCode, + string? Street, + uint? HouseNumber, + uint? FlatNumber +); \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/DTOs/Response/GetTicketDetailsResponseDto.cs b/TickAPI/TickAPI/Tickets/DTOs/Response/GetTicketDetailsResponseDto.cs index a0c6423..72adbd9 100644 --- a/TickAPI/TickAPI/Tickets/DTOs/Response/GetTicketDetailsResponseDto.cs +++ b/TickAPI/TickAPI/Tickets/DTOs/Response/GetTicketDetailsResponseDto.cs @@ -1,4 +1,6 @@ -namespace TickAPI.Tickets.DTOs.Response; +using TickAPI.Addresses.Models; + +namespace TickAPI.Tickets.DTOs.Response; public class GetTicketDetailsResponseDto { @@ -10,4 +12,6 @@ public class GetTicketDetailsResponseDto public string OrganizerName {get; set;} public DateTime StartDate { get; set; } public DateTime EndDate { get; set; } + + public GetTicketDetailsAddressDto Address { get; set; } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs b/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs index 89d0bf5..678bb4e 100644 --- a/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs +++ b/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs @@ -39,7 +39,8 @@ public async Task> CheckIfTicketBelongsToCustomerAsync(Guid id, str public async Task> GetTicketByIdAsync(Guid id) { var ticket = await _tickApiDbContext.Tickets.Include(t => t.Type).Include(t => t.Type.Event) - .Include(t => t.Type.Event.Organizer).FirstOrDefaultAsync(); + .Include(t => t.Type.Event.Organizer). + Include(t => t.Type.Event.Address).FirstOrDefaultAsync(); if (ticket == null) { return Result.Failure(StatusCodes.Status404NotFound, "Ticket with this id doesn't exist"); diff --git a/TickAPI/TickAPI/Tickets/Services/TicketService.cs b/TickAPI/TickAPI/Tickets/Services/TicketService.cs index 3cd03fc..8b381ec 100644 --- a/TickAPI/TickAPI/Tickets/Services/TicketService.cs +++ b/TickAPI/TickAPI/Tickets/Services/TicketService.cs @@ -45,6 +45,9 @@ public async Task> GetTicketDetailsAsync(str return Result.PropagateError(ticketRes); } var ticket = ticketRes.Value; + var ev = ticket.Type.Event; + var address = new GetTicketDetailsAddressDto(ev.Address.Country, ev.Address.City, ev.Address.PostalCode, + ev.Address.Street, ev.Address.HouseNumber, ev.Address.FlatNumber); var ticketDetails = new GetTicketDetailsResponseDto { NameOnTicket = ticket.NameOnTicket, @@ -55,6 +58,8 @@ public async Task> GetTicketDetailsAsync(str OrganizerName = ticket.Type.Event.Organizer.DisplayName, StartDate = ticket.Type.Event.StartDate, EndDate = ticket.Type.Event.EndDate, + Address = address, + }; return Result.Success(ticketDetails); From 64e56701fed724ab2b5746d61682d1c702bf2c2d Mon Sep 17 00:00:00 2001 From: kubapoke Date: Sat, 3 May 2025 00:57:59 +0200 Subject: [PATCH 16/80] added the base of ShoppingCartService.cs methods (headers still require changing) --- .../Abstractions/IShoppingCartService.cs | 9 ++++++-- .../Services/ShoppingCartService.cs | 23 +++++++++++++++++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs index 80e4e6d..c72e3a9 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs @@ -1,6 +1,11 @@ -namespace TickAPI.ShoppingCarts.Abstractions; +using TickAPI.Common.Results; + +namespace TickAPI.ShoppingCarts.Abstractions; public interface IShoppingCartService { - + public Task AddTicketAsync(); + public Task GetTicketsAsync(); + public Task RemoveTicketAsync(); + public Task CheckoutAsync(); } \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs index b1c9d15..8233773 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs @@ -1,8 +1,27 @@ -using TickAPI.ShoppingCarts.Abstractions; +using TickAPI.Common.Results; +using TickAPI.ShoppingCarts.Abstractions; namespace TickAPI.ShoppingCarts.Services; public class ShoppingCartService : IShoppingCartService { - + public Task AddTicketAsync() + { + throw new NotImplementedException(); + } + + public Task GetTicketsAsync() + { + throw new NotImplementedException(); + } + + public Task RemoveTicketAsync() + { + throw new NotImplementedException(); + } + + public Task CheckoutAsync() + { + throw new NotImplementedException(); + } } \ No newline at end of file From e6619a4d06f1c32f816196914da6d35e4ff8f606 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Sat, 3 May 2025 02:17:32 +0200 Subject: [PATCH 17/80] added acquiring cart contents (from service) and adding ticket to cart (via controller) --- .../Common/Results/ResultTests.cs | 25 ++++++++ TickAPI/TickAPI/Common/Results/Result.cs | 10 +++ TickAPI/TickAPI/Program.cs | 5 ++ .../Abstractions/IShoppingCartRepository.cs | 11 ++++ .../Abstractions/IShoppingCartService.cs | 2 +- .../Controllers/ShoppingCartsController.cs | 26 ++++++-- .../DTOs/Request/AddNewTicketDto.cs | 7 +++ .../ShoppingCarts/Models/ShoppingCart.cs | 3 +- .../Repositories/ShoppingCartRepository.cs | 61 +++++++++++++++++++ .../Services/ShoppingCartService.cs | 36 ++++++++++- TickAPI/TickAPI/TickAPI.csproj | 4 ++ ...CartTicket.cs => ShoppingCartNewTicket.cs} | 5 +- .../Models/ShoppingCartResellTicket.cs | 7 +++ 13 files changed, 191 insertions(+), 11 deletions(-) create mode 100644 TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartRepository.cs create mode 100644 TickAPI/TickAPI/ShoppingCarts/DTOs/Request/AddNewTicketDto.cs create mode 100644 TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs rename TickAPI/TickAPI/Tickets/Models/{ShoppingCartTicket.cs => ShoppingCartNewTicket.cs} (51%) create mode 100644 TickAPI/TickAPI/Tickets/Models/ShoppingCartResellTicket.cs diff --git a/TickAPI/TickAPI.Tests/Common/Results/ResultTests.cs b/TickAPI/TickAPI.Tests/Common/Results/ResultTests.cs index f09668d..0c26548 100644 --- a/TickAPI/TickAPI.Tests/Common/Results/ResultTests.cs +++ b/TickAPI/TickAPI.Tests/Common/Results/ResultTests.cs @@ -30,6 +30,21 @@ public void Failure_ShouldReturnResultWithError() Assert.Equal(statusCode, result.StatusCode); } + [Fact] + public void PropagateError_WhenResultWithErrorPassed_ShouldReturnResultWithError() + { + const int statusCode = 500; + const string errorMsg = "error message"; + var resultWithError = Result.Failure(statusCode, errorMsg); + + var result = Result.PropagateError(resultWithError); + + Assert.True(result.IsError); + Assert.False(result.IsSuccess); + Assert.Equal(errorMsg, result.ErrorMsg); + Assert.Equal(statusCode, result.StatusCode); + } + [Fact] public void PropagateError_WhenGenericResultWithErrorPassed_ShouldReturnResultWithError() { @@ -45,6 +60,16 @@ public void PropagateError_WhenGenericResultWithErrorPassed_ShouldReturnResultWi Assert.Equal(statusCode, result.StatusCode); } + [Fact] + public void PropagateError_WhenResultWithSuccessPassed_ShouldThrowArgumentException() + { + var resultWithSuccess = Result.Success(); + + var act = () => Result.PropagateError(resultWithSuccess); + + Assert.Throws(act); + } + [Fact] public void PropagateError_WhenGenericResultWithSuccessPassed_ShouldThrowArgumentException() { diff --git a/TickAPI/TickAPI/Common/Results/Result.cs b/TickAPI/TickAPI/Common/Results/Result.cs index d339858..3d03d84 100644 --- a/TickAPI/TickAPI/Common/Results/Result.cs +++ b/TickAPI/TickAPI/Common/Results/Result.cs @@ -26,6 +26,16 @@ public static Result Failure(int statusCode, string errorMsg) return new Result(false, statusCode, errorMsg); } + public static Result PropagateError(Result other) + { + if (other.IsSuccess) + { + throw new ArgumentException("Trying to propagate error from successful value"); + } + + return Failure(other.StatusCode, other.ErrorMsg); + } + public static Result PropagateError(Result other) { if (other.IsSuccess) diff --git a/TickAPI/TickAPI/Program.cs b/TickAPI/TickAPI/Program.cs index 26fe234..9180548 100644 --- a/TickAPI/TickAPI/Program.cs +++ b/TickAPI/TickAPI/Program.cs @@ -43,6 +43,7 @@ using TickAPI.Common.Payment.Health; using TickAPI.Common.Payment.Services; using TickAPI.ShoppingCarts.Abstractions; +using TickAPI.ShoppingCarts.Repositories; using TickAPI.ShoppingCarts.Services; // Builder constants @@ -118,6 +119,10 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); +// Add shopping cart services. +builder.Services.AddScoped(); +builder.Services.AddScoped(); + // Add common services. builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartRepository.cs b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartRepository.cs new file mode 100644 index 0000000..af2d2d8 --- /dev/null +++ b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartRepository.cs @@ -0,0 +1,11 @@ +using TickAPI.Common.Results; +using TickAPI.Common.Results.Generic; +using TickAPI.ShoppingCarts.Models; + +namespace TickAPI.ShoppingCarts.Abstractions; + +public interface IShoppingCartRepository +{ + public Task> GetShoppingCartByEmailAsync(string customerEmail); + public Task UpdateShoppingCartAsync(string customerEmail, ShoppingCart shoppingCart); +} \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs index c72e3a9..12103bb 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs @@ -4,7 +4,7 @@ namespace TickAPI.ShoppingCarts.Abstractions; public interface IShoppingCartService { - public Task AddTicketAsync(); + public Task AddNewTicketAsync(Guid ticketTypeId, string customerEmail, string? nameOnTicket, string? seats); public Task GetTicketsAsync(); public Task RemoveTicketAsync(); public Task CheckoutAsync(); diff --git a/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs b/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs index 5c19622..3f3e16a 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs @@ -1,7 +1,9 @@ using Microsoft.AspNetCore.Mvc; using TickAPI.Common.Auth.Attributes; using TickAPI.Common.Auth.Enums; +using TickAPI.Common.Claims.Abstractions; using TickAPI.ShoppingCarts.Abstractions; +using TickAPI.ShoppingCarts.DTOs.Request; namespace TickAPI.ShoppingCarts.Controllers; @@ -10,17 +12,33 @@ namespace TickAPI.ShoppingCarts.Controllers; public class ShoppingCartsController : ControllerBase { private readonly IShoppingCartService _shoppingCartService; + private readonly IClaimsService _claimsService; - public ShoppingCartsController(IShoppingCartService shoppingCartService) + public ShoppingCartsController(IShoppingCartService shoppingCartService, IClaimsService claimsService) { _shoppingCartService = shoppingCartService; + _claimsService = claimsService; } [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] - [HttpPost("ticket")] - public async Task AddTicket() + [HttpPost] + public async Task AddTicket([FromBody] AddNewTicketDto addNewTicketDto) { - throw new NotImplementedException(); + var emailResult = _claimsService.GetEmailFromClaims(User.Claims); + if (emailResult.IsError) + { + return StatusCode(emailResult.StatusCode, emailResult.ErrorMsg); + } + var email = emailResult.Value!; + + var addTicketResult = await _shoppingCartService.AddNewTicketAsync(addNewTicketDto.TicketTypeId, email, + addNewTicketDto.NameOnTicket, addNewTicketDto.Seats); + if (addTicketResult.IsError) + { + return StatusCode(addTicketResult.StatusCode, addTicketResult.ErrorMsg); + } + + return Ok(); } [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] diff --git a/TickAPI/TickAPI/ShoppingCarts/DTOs/Request/AddNewTicketDto.cs b/TickAPI/TickAPI/ShoppingCarts/DTOs/Request/AddNewTicketDto.cs new file mode 100644 index 0000000..de86962 --- /dev/null +++ b/TickAPI/TickAPI/ShoppingCarts/DTOs/Request/AddNewTicketDto.cs @@ -0,0 +1,7 @@ +namespace TickAPI.ShoppingCarts.DTOs.Request; + +public record AddNewTicketDto( + Guid TicketTypeId, + string? NameOnTicket, + string? Seats +); \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Models/ShoppingCart.cs b/TickAPI/TickAPI/ShoppingCarts/Models/ShoppingCart.cs index c734465..2ed9b5f 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Models/ShoppingCart.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Models/ShoppingCart.cs @@ -4,5 +4,6 @@ namespace TickAPI.ShoppingCarts.Models; public class ShoppingCart { - public List Tickets { get; set; } + public List Tickets { get; set; } = []; + public List ResellTickets { get; set; } = []; } \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs b/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs new file mode 100644 index 0000000..6cda1c9 --- /dev/null +++ b/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs @@ -0,0 +1,61 @@ +using TickAPI.Common.Redis.Abstractions; +using TickAPI.Common.Results; +using TickAPI.Common.Results.Generic; +using TickAPI.ShoppingCarts.Abstractions; +using TickAPI.ShoppingCarts.Models; + +namespace TickAPI.ShoppingCarts.Repositories; + +public class ShoppingCartRepository : IShoppingCartRepository +{ + private readonly IRedisService _redisService; + private static readonly TimeSpan DefaultExpiry = TimeSpan.FromMinutes(15); + + public ShoppingCartRepository(IRedisService redisService) + { + _redisService = redisService; + } + + public async Task> GetShoppingCartByEmailAsync(string customerEmail) + { + var cartKey = GetCartKey(customerEmail); + ShoppingCart? cart; + + try + { + cart = await _redisService.GetObjectAsync(cartKey); + await _redisService.KeyExpireAsync(cartKey, DefaultExpiry); + } + catch (Exception e) + { + return Result.Failure(StatusCodes.Status500InternalServerError, e.Message); + } + + return Result.Success(cart ?? new ShoppingCart()); + } + + public async Task UpdateShoppingCartAsync(string customerEmail, ShoppingCart shoppingCart) + { + var cartKey = GetCartKey(customerEmail); + + try + { + var res = await _redisService.SetObjectAsync(cartKey, shoppingCart, DefaultExpiry); + if (!res) + { + return Result.Failure(StatusCodes.Status500InternalServerError, "The shopping cart could not be updated."); + } + } + catch (Exception e) + { + return Result.Failure(StatusCodes.Status500InternalServerError, e.Message); + } + + return Result.Success(); + } + + private static string GetCartKey(string customerEmail) + { + return $"cart:{customerEmail}"; + } +} \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs index 8233773..a02c8ee 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs @@ -1,13 +1,45 @@ using TickAPI.Common.Results; +using TickAPI.Common.Results.Generic; using TickAPI.ShoppingCarts.Abstractions; +using TickAPI.Tickets.Models; namespace TickAPI.ShoppingCarts.Services; public class ShoppingCartService : IShoppingCartService { - public Task AddTicketAsync() + private readonly IShoppingCartRepository _shoppingCartRepository; + + public ShoppingCartService(IShoppingCartRepository shoppingCartRepository) { - throw new NotImplementedException(); + _shoppingCartRepository = shoppingCartRepository; + } + + public async Task AddNewTicketAsync(Guid ticketTypeId, string customerEmail, string? nameOnTicket, string? seats) + { + var getShoppingCartResult = await _shoppingCartRepository.GetShoppingCartByEmailAsync(customerEmail); + + if (getShoppingCartResult.IsError) + { + return Result.PropagateError(getShoppingCartResult); + } + + var cart = getShoppingCartResult.Value!; + + cart.Tickets.Add(new ShoppingCartNewTicket() + { + TicketTypeId = ticketTypeId, + NameOnTicket = nameOnTicket, + Seats = seats, + }); + + var updateShoppingCartResult = await _shoppingCartRepository.UpdateShoppingCartAsync(customerEmail, cart); + + if (updateShoppingCartResult.IsError) + { + return Result.PropagateError(updateShoppingCartResult); + } + + return Result.Success(); } public Task GetTicketsAsync() diff --git a/TickAPI/TickAPI/TickAPI.csproj b/TickAPI/TickAPI/TickAPI.csproj index a4ca3a8..4ee1745 100644 --- a/TickAPI/TickAPI/TickAPI.csproj +++ b/TickAPI/TickAPI/TickAPI.csproj @@ -23,6 +23,10 @@ + + + + diff --git a/TickAPI/TickAPI/Tickets/Models/ShoppingCartTicket.cs b/TickAPI/TickAPI/Tickets/Models/ShoppingCartNewTicket.cs similarity index 51% rename from TickAPI/TickAPI/Tickets/Models/ShoppingCartTicket.cs rename to TickAPI/TickAPI/Tickets/Models/ShoppingCartNewTicket.cs index c19904b..6521bc0 100644 --- a/TickAPI/TickAPI/Tickets/Models/ShoppingCartTicket.cs +++ b/TickAPI/TickAPI/Tickets/Models/ShoppingCartNewTicket.cs @@ -1,9 +1,8 @@ namespace TickAPI.Tickets.Models; -public class ShoppingCartTicket +public class ShoppingCartNewTicket { public Guid TicketTypeId { get; set; } - public Guid CustomerId { get; set; } - public string NameOnTicket { get; set; } + public string? NameOnTicket { get; set; } public string? Seats { get; set; } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Models/ShoppingCartResellTicket.cs b/TickAPI/TickAPI/Tickets/Models/ShoppingCartResellTicket.cs new file mode 100644 index 0000000..81fc5a4 --- /dev/null +++ b/TickAPI/TickAPI/Tickets/Models/ShoppingCartResellTicket.cs @@ -0,0 +1,7 @@ +namespace TickAPI.Tickets.Models; + +public class ShoppingCartResellTicket +{ + public Guid TicketId { get; set; } + public string? NameOnTicket { get; set; } +} \ No newline at end of file From 8a74425ca674b91284bd6183eec9b845aa7a6283 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Sat, 3 May 2025 09:50:08 +0200 Subject: [PATCH 18/80] Add unit tests --- .../Tickets/Services/TicketServiceTests.cs | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs b/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs index 068a29f..6270cbf 100644 --- a/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs @@ -1,7 +1,13 @@ using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; using Moq; +using TickAPI.Addresses.Models; using TickAPI.Common.Results.Generic; +using TickAPI.Customers.Models; +using TickAPI.Events.Models; +using TickAPI.Organizers.Models; using TickAPI.Tickets.Abstractions; +using TickAPI.Tickets.DTOs.Response; using TickAPI.Tickets.Models; using TickAPI.Tickets.Services; using TickAPI.TicketTypes.Models; @@ -56,4 +62,103 @@ public void GetNumberOfAvailableTicketsByType_WhenMoreTicketExistThanMaxCount_Sh Assert.Equal(StatusCodes.Status500InternalServerError, result.StatusCode); Assert.Equal("The number of available tickets is negative.", result.ErrorMsg); } + + [Fact] + public async Task GetTicketDetailsAsync_WhenTicketExistsForTheUser_ShouldReturnTicketDetails() + { + + // Arrange + var ticket = new Ticket + { + Id = Guid.NewGuid(), + ForResell = false, + NameOnTicket = "NameOnTicket", + Seats = null, + Type = new TicketType + { + Currency = "USD", + Price = 20, + Event = new Event + { + Name = "EventName", + StartDate = new DateTime(2025, 10, 10), + EndDate = new DateTime(2025, 10, 20), + Organizer = new Organizer + { + DisplayName = "organizerName", + }, + Address = new Address + { + City = "Warsaw", + Country = "Poland", + PostalCode = "12345", + FlatNumber = null, + HouseNumber = null, + Street = "Street", + } + } + }, + }; + string email = "123@123.com"; + + Mock ticketRepositoryMock = new Mock(); + ticketRepositoryMock.Setup(m => + m.CheckIfTicketBelongsToCustomerAsync(ticket.Id, email)).ReturnsAsync(Result.Success(true)); + + ticketRepositoryMock.Setup(m => m.GetTicketByIdAsync(ticket.Id)).ReturnsAsync(Result.Success(ticket)); + + var sut = new TicketService(ticketRepositoryMock.Object); + + // Act + + var res = await sut.GetTicketDetailsAsync(email, ticket.Id); + + // Assert + + Assert.True(res.IsSuccess); + var details = res.Value; + Assert.NotNull(details); + + Assert.Equal(ticket.NameOnTicket, details.NameOnTicket); + Assert.Equal(ticket.Seats, details.Seats); + Assert.Equal(ticket.Type.Currency, details.Currency); + Assert.Equal(ticket.Type.Price, details.Price); + Assert.Equal(ticket.Type.Event.StartDate, details.StartDate); + Assert.Equal(ticket.Type.Event.EndDate, details.EndDate); + Assert.Equal(ticket.Type.Event.Organizer.DisplayName, details.OrganizerName); + Assert.Equal(ticket.Type.Event.Address.Street, details.Address.Street); + Assert.Equal(ticket.Type.Event.Address.HouseNumber, details.Address.HouseNumber); + Assert.Equal(ticket.Type.Event.Address.FlatNumber, details.Address.FlatNumber); + Assert.Equal(ticket.Type.Event.Address.PostalCode, details.Address.PostalCode); + Assert.Equal(ticket.Type.Event.Address.City, details.Address.City); + Assert.Equal(ticket.Type.Event.Address.Country, details.Address.Country); + + } + + [Fact] + public async Task GetTicketDetailsAsync_WhenTicketDoesntExistsForTheUser_ShouldReturnError() + { + + // Arrange + + Guid ticketId = Guid.NewGuid(); + string email = "123@123.com"; + + Mock ticketRepositoryMock = new Mock(); + ticketRepositoryMock.Setup(m => m.CheckIfTicketBelongsToCustomerAsync(ticketId, email)). + ReturnsAsync(Result.Failure(StatusCodes.Status404NotFound, "Ticket with this id doesn't exist " + + "for this user")); + + var sut = new TicketService(ticketRepositoryMock.Object); + + // Act + + var res = await sut.GetTicketDetailsAsync(email, ticketId); + + // Assert + + Assert.False(res.IsSuccess); + Assert.Equal(StatusCodes.Status404NotFound, res.StatusCode); + Assert.Equal("Ticket with this id doesn't exist for this user", res.ErrorMsg); + } } \ No newline at end of file From dd952cea6555a195fc869e1e741bd3da50972983 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Sat, 3 May 2025 15:09:28 +0200 Subject: [PATCH 19/80] Fixes from review --- .../Tickets/Services/TicketServiceTests.cs | 15 ++++---- .../Events/Repositories/EventRepository.cs | 1 - .../TickAPI/Events/Services/EventService.cs | 1 - .../Tickets/Abstractions/ITicketRepository.cs | 3 +- .../Tickets/Abstractions/ITicketService.cs | 5 ++- .../Tickets/Controllers/TicketsController.cs | 2 +- .../Response/GetTicketDetailsResponseDto.cs | 26 +++++++------- .../Tickets/Repositories/TicketRepository.cs | 24 +++---------- .../TickAPI/Tickets/Services/TicketService.cs | 36 ++++++++----------- 9 files changed, 43 insertions(+), 70 deletions(-) diff --git a/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs b/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs index 9b56530..3b4c3db 100644 --- a/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs @@ -337,6 +337,7 @@ public async Task GetTicketDetailsAsync_WhenTicketExistsForTheUser_ShouldReturnT { // Arrange + var eventGuid = Guid.NewGuid(); var ticket = new Ticket { Id = Guid.NewGuid(), @@ -345,6 +346,7 @@ public async Task GetTicketDetailsAsync_WhenTicketExistsForTheUser_ShouldReturnT Seats = null, Type = new TicketType { + Id = eventGuid, Currency = "USD", Price = 20, Event = new Event @@ -371,18 +373,17 @@ public async Task GetTicketDetailsAsync_WhenTicketExistsForTheUser_ShouldReturnT string email = "123@123.com"; Mock ticketRepositoryMock = new Mock(); - ticketRepositoryMock.Setup(m => - m.CheckIfTicketBelongsToCustomerAsync(ticket.Id, email)).ReturnsAsync(Result.Success(true)); var paginationServiceMock = new Mock(); - ticketRepositoryMock.Setup(m => m.GetTicketByIdAsync(ticket.Id)).ReturnsAsync(Result.Success(ticket)); + ticketRepositoryMock.Setup(m => m.GetTicketWithDetailsByIdAndEmailAsync(ticket.Id, email)) + .ReturnsAsync(Result.Success(ticket)); var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object); // Act - var res = await sut.GetTicketDetailsAsync(email, ticket.Id); + var res = await sut.GetTicketDetailsAsync(ticket.Id, email); // Assert @@ -416,8 +417,8 @@ public async Task GetTicketDetailsAsync_WhenTicketDoesntExistsForTheUser_ShouldR string email = "123@123.com"; Mock ticketRepositoryMock = new Mock(); - ticketRepositoryMock.Setup(m => m.CheckIfTicketBelongsToCustomerAsync(ticketId, email)). - ReturnsAsync(Result.Failure(StatusCodes.Status404NotFound, "Ticket with this id doesn't exist " + + ticketRepositoryMock.Setup(m => m.GetTicketWithDetailsByIdAndEmailAsync(ticketId, email)). + ReturnsAsync(Result.Failure(StatusCodes.Status404NotFound, "Ticket with this id doesn't exist " + "for this user")); var paginationServiceMock = new Mock(); @@ -425,7 +426,7 @@ public async Task GetTicketDetailsAsync_WhenTicketDoesntExistsForTheUser_ShouldR // Act - var res = await sut.GetTicketDetailsAsync(email, ticketId); + var res = await sut.GetTicketDetailsAsync(ticketId, email); // Assert diff --git a/TickAPI/TickAPI/Events/Repositories/EventRepository.cs b/TickAPI/TickAPI/Events/Repositories/EventRepository.cs index c151d62..bdc59ee 100644 --- a/TickAPI/TickAPI/Events/Repositories/EventRepository.cs +++ b/TickAPI/TickAPI/Events/Repositories/EventRepository.cs @@ -54,5 +54,4 @@ public async Task> GetEventByIdAsync(Guid eventId) return Result.Success(@event); } - } \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/Services/EventService.cs b/TickAPI/TickAPI/Events/Services/EventService.cs index a226bd9..201a9a9 100644 --- a/TickAPI/TickAPI/Events/Services/EventService.cs +++ b/TickAPI/TickAPI/Events/Services/EventService.cs @@ -199,5 +199,4 @@ private static GetEventResponseDto MapEventToGetEventResponseDto(Event ev) return new GetEventResponseDto(ev.Id, ev.Name, ev.Description, ev.StartDate, ev.EndDate, ev.MinimumAge, minimumPrice, maximumPrice, categories, ev.EventStatus, address); } - } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs b/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs index f0a4733..93e313f 100644 --- a/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs +++ b/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs @@ -7,8 +7,7 @@ namespace TickAPI.Tickets.Abstractions; public interface ITicketRepository { public IQueryable GetAllTicketsByTicketType(TicketType ticketType); - public Task> CheckIfTicketBelongsToCustomerAsync(Guid id, string email); - public Task> GetTicketByIdAsync(Guid id); + public Task> GetTicketWithDetailsByIdAndEmailAsync(Guid id, string email); public IQueryable GetTicketsByEventId(Guid eventId); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs b/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs index dd63ef9..f705dce 100644 --- a/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs +++ b/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs @@ -8,8 +8,7 @@ namespace TickAPI.Tickets.Abstractions; public interface ITicketService { public Result GetNumberOfAvailableTicketsByType(TicketType ticketType); - public Task> GetTicketDetailsAsync(string email, Guid ticketGuid); - - Task>> GetTicketsForResellAsync(Guid eventId, int page, + public Task> GetTicketDetailsAsync(Guid ticketGuid, string email); + public Task>> GetTicketsForResellAsync(Guid eventId, int page, int pageSize); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs b/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs index 82f7d25..dfc56b0 100644 --- a/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs +++ b/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs @@ -32,7 +32,7 @@ public async Task> GetTicketDetails(Gu return StatusCode(emailResult.StatusCode, emailResult.ErrorMsg); } var email = emailResult.Value!; - var ticket = await _ticketService.GetTicketDetailsAsync(email, id); + var ticket = await _ticketService.GetTicketDetailsAsync(id, email); if (!ticket.IsSuccess) { return StatusCode(ticket.StatusCode, ticket.ErrorMsg); diff --git a/TickAPI/TickAPI/Tickets/DTOs/Response/GetTicketDetailsResponseDto.cs b/TickAPI/TickAPI/Tickets/DTOs/Response/GetTicketDetailsResponseDto.cs index 72adbd9..46032d5 100644 --- a/TickAPI/TickAPI/Tickets/DTOs/Response/GetTicketDetailsResponseDto.cs +++ b/TickAPI/TickAPI/Tickets/DTOs/Response/GetTicketDetailsResponseDto.cs @@ -2,16 +2,16 @@ namespace TickAPI.Tickets.DTOs.Response; -public class GetTicketDetailsResponseDto -{ - public string NameOnTicket { get; set; } - public string? Seats { get; set; } - public decimal Price { get; set; } - public string Currency { get; set; } - public string EventName {get; set;} - public string OrganizerName {get; set;} - public DateTime StartDate { get; set; } - public DateTime EndDate { get; set; } - - public GetTicketDetailsAddressDto Address { get; set; } -} \ No newline at end of file +public record GetTicketDetailsResponseDto +( + string NameOnTicket, + string? Seats, + decimal Price, + string Currency, + string EventName, + string OrganizerName, + DateTime StartDate, + DateTime EndDate, + GetTicketDetailsAddressDto Address, + Guid eventId +); \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs b/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs index 8e1e60a..c5a6a0d 100644 --- a/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs +++ b/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs @@ -1,7 +1,5 @@ using Microsoft.EntityFrameworkCore; -using TickAPI.Common.Results; using TickAPI.Common.Results.Generic; -using Microsoft.EntityFrameworkCore; using TickAPI.Common.TickApiDbContext; using TickAPI.Tickets.Abstractions; using TickAPI.Tickets.Models; @@ -30,26 +28,12 @@ public IQueryable GetTicketsByEventId(Guid eventId) .Include(t => t.Type.Event) .Where(t => t.Type.Event.Id == eventId); } - - public async Task> CheckIfTicketBelongsToCustomerAsync(Guid id, string email) - { - var count = await _tickApiDbContext.Tickets.Join(_tickApiDbContext.Customers, - ticket => ticket.Owner.Id, customer => customer.Id, (ticket, customer) => new {ticket.Id}).CountAsync(); - - if (count > 0) - { - return Result.Success(true); - } - - return Result.Failure(StatusCodes.Status404NotFound, "Ticket with this id doesn't exist " + - "for this user"); - } - - public async Task> GetTicketByIdAsync(Guid id) + + public async Task> GetTicketWithDetailsByIdAndEmailAsync(Guid id, string email) { var ticket = await _tickApiDbContext.Tickets.Include(t => t.Type).Include(t => t.Type.Event) - .Include(t => t.Type.Event.Organizer). - Include(t => t.Type.Event.Address).FirstOrDefaultAsync(); + .Include(t => t.Type.Event.Organizer).Include(t => t.Type.Event.Address) + .Where(t => (t.Id == id && t.Owner.Email == email)).FirstOrDefaultAsync(); if (ticket == null) { return Result.Failure(StatusCodes.Status404NotFound, "Ticket with this id doesn't exist"); diff --git a/TickAPI/TickAPI/Tickets/Services/TicketService.cs b/TickAPI/TickAPI/Tickets/Services/TicketService.cs index 6c85849..6843b04 100644 --- a/TickAPI/TickAPI/Tickets/Services/TicketService.cs +++ b/TickAPI/TickAPI/Tickets/Services/TicketService.cs @@ -48,15 +48,9 @@ public async Task>> GetTicke return Result>.Success(paginatedResult); } - public async Task> GetTicketDetailsAsync(string email, Guid ticketGuid) + public async Task> GetTicketDetailsAsync(Guid ticketGuid, string email) { - var exists = await _ticketRepository.CheckIfTicketBelongsToCustomerAsync(ticketGuid, email); - if (!exists.IsSuccess) - { - return Result.PropagateError(exists); - } - - var ticketRes = await _ticketRepository.GetTicketByIdAsync(ticketGuid); + var ticketRes = await _ticketRepository.GetTicketWithDetailsByIdAndEmailAsync(ticketGuid, email); if (!ticketRes.IsSuccess) { return Result.PropagateError(ticketRes); @@ -66,20 +60,18 @@ public async Task> GetTicketDetailsAsync(str var address = new GetTicketDetailsAddressDto(ev.Address.Country, ev.Address.City, ev.Address.PostalCode, ev.Address.Street, ev.Address.HouseNumber, ev.Address.FlatNumber); var ticketDetails = new GetTicketDetailsResponseDto - { - NameOnTicket = ticket.NameOnTicket, - Seats = ticket.Seats, - Price = ticket.Type.Price, - Currency = ticket.Type.Currency, - EventName = ticket.Type.Event.Name, - OrganizerName = ticket.Type.Event.Organizer.DisplayName, - StartDate = ticket.Type.Event.StartDate, - EndDate = ticket.Type.Event.EndDate, - Address = address, - - }; - + ( + ticket.NameOnTicket, + ticket.Seats, + ticket.Type.Price, + ticket.Type.Currency, + ticket.Type.Event.Name, + ticket.Type.Event.Organizer.DisplayName, + ticket.Type.Event.StartDate, + ticket.Type.Event.EndDate, + address, + ticket.Type.Event.Id + ); return Result.Success(ticketDetails); - } } \ No newline at end of file From 8ce7ff43ae3de09c1065f019a74be1faaf830e89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Sat, 3 May 2025 17:42:24 +0200 Subject: [PATCH 20/80] Dont even want to comment on it --- TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs b/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs index dfc56b0..672368c 100644 --- a/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs +++ b/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs @@ -5,8 +5,6 @@ using TickAPI.Tickets.Abstractions; using TickAPI.Tickets.DTOs.Response; using TickAPI.Common.Pagination.Responses; -using TickAPI.Tickets.Abstractions; -using TickAPI.Tickets.DTOs.Response; namespace TickAPI.Tickets.Controllers; @@ -33,7 +31,7 @@ public async Task> GetTicketDetails(Gu } var email = emailResult.Value!; var ticket = await _ticketService.GetTicketDetailsAsync(id, email); - if (!ticket.IsSuccess) + if (ticket.IsError) { return StatusCode(ticket.StatusCode, ticket.ErrorMsg); } From 2bb7179b71a9f94621c3846aa24b2e7702bd10b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Sat, 3 May 2025 19:11:17 +0200 Subject: [PATCH 21/80] Dont want to comment on it even harder --- TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs | 2 +- TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs | 1 - TickAPI/TickAPI/Tickets/Services/TicketService.cs | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs b/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs index 3b4c3db..29c7a2d 100644 --- a/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs @@ -408,7 +408,7 @@ public async Task GetTicketDetailsAsync_WhenTicketExistsForTheUser_ShouldReturnT } [Fact] - public async Task GetTicketDetailsAsync_WhenTicketDoesntExistsForTheUser_ShouldReturnError() + public async Task GetTicketDetailsAsync_WhenTicketDoesNotExistForTheUser_ShouldReturnError() { // Arrange diff --git a/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs b/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs index 93e313f..ed24346 100644 --- a/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs +++ b/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs @@ -7,7 +7,6 @@ namespace TickAPI.Tickets.Abstractions; public interface ITicketRepository { public IQueryable GetAllTicketsByTicketType(TicketType ticketType); - public Task> GetTicketWithDetailsByIdAndEmailAsync(Guid id, string email); public IQueryable GetTicketsByEventId(Guid eventId); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Services/TicketService.cs b/TickAPI/TickAPI/Tickets/Services/TicketService.cs index 6843b04..f6c77ac 100644 --- a/TickAPI/TickAPI/Tickets/Services/TicketService.cs +++ b/TickAPI/TickAPI/Tickets/Services/TicketService.cs @@ -51,11 +51,11 @@ public async Task>> GetTicke public async Task> GetTicketDetailsAsync(Guid ticketGuid, string email) { var ticketRes = await _ticketRepository.GetTicketWithDetailsByIdAndEmailAsync(ticketGuid, email); - if (!ticketRes.IsSuccess) + if (ticketRes.IsError) { return Result.PropagateError(ticketRes); } - var ticket = ticketRes.Value; + var ticket = ticketRes.Value!; var ev = ticket.Type.Event; var address = new GetTicketDetailsAddressDto(ev.Address.Country, ev.Address.City, ev.Address.PostalCode, ev.Address.Street, ev.Address.HouseNumber, ev.Address.FlatNumber); From 410b2dd77a3b48a9823a4c5cd63a5e0694810baf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Sun, 4 May 2025 13:49:19 +0200 Subject: [PATCH 22/80] Working endpoint --- .../Tickets/Abstractions/ITicketRepository.cs | 1 + .../Tickets/Abstractions/ITicketService.cs | 2 ++ .../Tickets/Controllers/TicketsController.cs | 17 +++++++++++++++++ .../DTOs/Response/GetTicketForCustomerDto.cs | 8 ++++++++ .../Tickets/Repositories/TicketRepository.cs | 9 +++++++++ .../TickAPI/Tickets/Services/TicketService.cs | 15 +++++++++++++++ 6 files changed, 52 insertions(+) create mode 100644 TickAPI/TickAPI/Tickets/DTOs/Response/GetTicketForCustomerDto.cs diff --git a/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs b/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs index ed24346..a09c57d 100644 --- a/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs +++ b/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs @@ -9,4 +9,5 @@ public interface ITicketRepository public IQueryable GetAllTicketsByTicketType(TicketType ticketType); public Task> GetTicketWithDetailsByIdAndEmailAsync(Guid id, string email); public IQueryable GetTicketsByEventId(Guid eventId); + public IQueryable GetTicketsByCustomerEmail(string email); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs b/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs index f705dce..37b6384 100644 --- a/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs +++ b/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs @@ -11,4 +11,6 @@ public interface ITicketService public Task> GetTicketDetailsAsync(Guid ticketGuid, string email); public Task>> GetTicketsForResellAsync(Guid eventId, int page, int pageSize); + public Task>> GetTicketsForCustomerAsync(string email, int page, + int pageSize); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs b/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs index 672368c..78979fb 100644 --- a/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs +++ b/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs @@ -48,4 +48,21 @@ public async Task>> Ge } return result.Value!; } + + [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] + [HttpGet] + public async Task>> GetTicketsForCustomer([FromQuery] int pageSize, [FromQuery] int page) + { + var emailResult = _claimsService.GetEmailFromClaims(User.Claims); + if (emailResult.IsError) + { + return StatusCode(emailResult.StatusCode, emailResult.ErrorMsg); + } + var tickets = await _ticketService.GetTicketsForCustomerAsync(emailResult.Value!, page, pageSize); + if (tickets.IsError) + { + return StatusCode(tickets.StatusCode, tickets.ErrorMsg); + } + return Ok(tickets.Value); + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/DTOs/Response/GetTicketForCustomerDto.cs b/TickAPI/TickAPI/Tickets/DTOs/Response/GetTicketForCustomerDto.cs new file mode 100644 index 0000000..ca94458 --- /dev/null +++ b/TickAPI/TickAPI/Tickets/DTOs/Response/GetTicketForCustomerDto.cs @@ -0,0 +1,8 @@ +namespace TickAPI.Tickets.DTOs.Response; + +public record GetTicketForCustomerDto +( + string EventName, + DateTime EventStartDate, + DateTime EventEndDate +); \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs b/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs index c5a6a0d..2ec02c2 100644 --- a/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs +++ b/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs @@ -28,6 +28,15 @@ public IQueryable GetTicketsByEventId(Guid eventId) .Include(t => t.Type.Event) .Where(t => t.Type.Event.Id == eventId); } + + public IQueryable GetTicketsByCustomerEmail(string email) + { + return _tickApiDbContext.Tickets + .Include(t => t.Owner) + .Include(t => t.Type) + .Include(t =>t.Type.Event) + .Where(t => t.Owner.Email == email); + } public async Task> GetTicketWithDetailsByIdAndEmailAsync(Guid id, string email) { diff --git a/TickAPI/TickAPI/Tickets/Services/TicketService.cs b/TickAPI/TickAPI/Tickets/Services/TicketService.cs index f6c77ac..d003a6e 100644 --- a/TickAPI/TickAPI/Tickets/Services/TicketService.cs +++ b/TickAPI/TickAPI/Tickets/Services/TicketService.cs @@ -47,6 +47,21 @@ public async Task>> GetTicke t => new GetTicketForResellResponseDto(t.Id, t.Type.Price, t.Type.Currency, t.Type.Description, t.Seats)); return Result>.Success(paginatedResult); } + //TODO: Maybe apply some filtering over here? + public async Task>> GetTicketsForCustomerAsync(string email, int page, int pageSize) + { + var customerTickets = _ticketRepository.GetTicketsByCustomerEmail(email); + var paginatedCustomerTickets = await _paginationService.PaginateAsync(customerTickets, pageSize, page); + if (paginatedCustomerTickets.IsError) + { + return Result>.PropagateError(paginatedCustomerTickets); + } + + var paginatedResult = _paginationService.MapData(paginatedCustomerTickets.Value!, + t => new GetTicketForCustomerDto(t.Type.Event.Name, t.Type.Event.StartDate, t.Type.Event.EndDate)); + + return Result>.Success(paginatedResult); + } public async Task> GetTicketDetailsAsync(Guid ticketGuid, string email) { From 08611621a581275c4ae2040d59b4993ecbaff1c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Sun, 4 May 2025 14:18:22 +0200 Subject: [PATCH 23/80] Add tests --- .../Tickets/Services/TicketServiceTests.cs | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs b/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs index 29c7a2d..479c287 100644 --- a/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs @@ -434,4 +434,118 @@ public async Task GetTicketDetailsAsync_WhenTicketDoesNotExistForTheUser_ShouldR Assert.Equal(StatusCodes.Status404NotFound, res.StatusCode); Assert.Equal("Ticket with this id doesn't exist for this user", res.ErrorMsg); } + + [Fact] + public async Task GetTicketsForCustomerAsync_WithValidInput_ReturnsSuccessResult() + { + // Arrange + var email = "test@example.com"; + var page = 0; + var pageSize = 10; + + var tickets = new List + { + new Ticket + { + Type = new TicketType + { + Event = new Event + { + Name = "EventName", + StartDate = new DateTime(2025, 10, 10), + EndDate = new DateTime(2025, 10, 20), + } + } + }, + new Ticket + { + Type = new TicketType + { + Event = new Event + { + Name = "EventName2", + StartDate = new DateTime(2025, 11, 10), + EndDate = new DateTime(2025, 11, 20), + } + } + } + }; + + var paginatedData = new PaginatedData + ( + tickets, + page, + pageSize, + false, + false, + new PaginationDetails(0, 2) + ); + var mappedData1 = new GetTicketForCustomerDto("EventName", new DateTime(2025, 10, 10), new DateTime(2025, 10, 20)); + var mappedData2 = new GetTicketForCustomerDto("EventName2", new DateTime(2025, 11, 10), new DateTime(2025, 11, 20)); + var mappedPaginatedData = new PaginatedData + ( + new List{mappedData1, mappedData2}, + page, + pageSize, + false, + false, + new PaginationDetails(0, 2) + ); + + var ticketRepositoryMock = new Mock(); + ticketRepositoryMock.Setup(r => r.GetTicketsByCustomerEmail(email)).Returns(tickets.AsQueryable()); + + var paginationServiceMock = new Mock(); + paginationServiceMock.Setup(p => p.PaginateAsync(tickets.AsQueryable(), pageSize, page)) + .ReturnsAsync(Result>.Success(paginatedData)); + + paginationServiceMock.Setup(p => p.MapData(paginatedData, It.IsAny>())) + .Returns(mappedPaginatedData); + var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object); + + // Act + var result = await sut.GetTicketsForCustomerAsync(email, page, pageSize); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(mappedPaginatedData, result.Value); + Assert.Equal(mappedData1, result.Value!.Data[0]); + Assert.Equal(mappedData2, result.Value!.Data[1]); + } + + [Fact] + public async Task GetTicketsForCustomerAsync_WhenUserHasNoTickets_ReturnsEmptyPagination() + { + // Arrange + var email = "empty@example.com"; + var page = 0; + var pageSize = 10; + + var emptyTickets = new List(); + + var emptyPaginatedData = new PaginatedData(emptyTickets, page, pageSize, + false, false, new PaginationDetails(0, 0)); + + var paginatedResult = Result>.Success(emptyPaginatedData); + + var mappedEmptyPaginatedData = new PaginatedData(new List(), + page, pageSize, false, false, new PaginationDetails(0, 0)); + + var ticketRepositoryMock = new Mock(); + ticketRepositoryMock.Setup(r => r.GetTicketsByCustomerEmail(email)).Returns(emptyTickets.AsQueryable()); + + var paginationServiceMock = new Mock(); + paginationServiceMock.Setup(p => p.PaginateAsync(emptyTickets.AsQueryable(), pageSize, page)).ReturnsAsync(paginatedResult); + paginationServiceMock.Setup(p => p.MapData(emptyPaginatedData, It.IsAny>())) + .Returns(mappedEmptyPaginatedData); + + var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object); + + // Act + var result = await sut.GetTicketsForCustomerAsync(email, page, pageSize); + + // Assert + Assert.True(result.IsSuccess); + Assert.Empty(result.Value!.Data); + } } \ No newline at end of file From bc1b0c2ed88f0fbd149a2c3e9c0edf3c56c76f83 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Sun, 4 May 2025 22:13:26 +0200 Subject: [PATCH 24/80] Create `ToObjectResult` for `Result` and `Result` --- .../Common/Results/Generic/ResultTests.cs | 87 ++++++++++++++++++- .../Common/Results/ResultTests.cs | 63 +++++++++++++- .../TickAPI/Common/Results/Generic/Result.cs | 20 ++++- TickAPI/TickAPI/Common/Results/Result.cs | 19 +++- 4 files changed, 185 insertions(+), 4 deletions(-) diff --git a/TickAPI/TickAPI.Tests/Common/Results/Generic/ResultTests.cs b/TickAPI/TickAPI.Tests/Common/Results/Generic/ResultTests.cs index ffb1390..e343a6a 100644 --- a/TickAPI/TickAPI.Tests/Common/Results/Generic/ResultTests.cs +++ b/TickAPI/TickAPI.Tests/Common/Results/Generic/ResultTests.cs @@ -1,4 +1,6 @@ -using TickAPI.Common.Results; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using TickAPI.Common.Results; using TickAPI.Common.Results.Generic; namespace TickAPI.Tests.Common.Results.Generic; @@ -8,10 +10,13 @@ public class ResultTests [Fact] public void Success_ShouldReturnResultWithValue() { + // Arrange const int value = 123; + // Act var result = Result.Success(value); + // Assert Assert.Equal(value, result.Value); Assert.True(result.IsSuccess); Assert.False(result.IsError); @@ -22,11 +27,14 @@ public void Success_ShouldReturnResultWithValue() [Fact] public void Failure_ShouldReturnResultWithError() { + // Arrange const int statusCode = 500; const string errorMsg = "example error msg"; + // Act var result = Result.Failure(500, errorMsg); + // Assert Assert.True(result.IsError); Assert.False(result.IsSuccess); Assert.Equal(errorMsg, result.ErrorMsg); @@ -36,12 +44,15 @@ public void Failure_ShouldReturnResultWithError() [Fact] public void PropagateError_WhenResultWithErrorPassed_ShouldReturnResultWithError() { + // Arrange const int statusCode = 500; const string errorMsg = "error message"; var resultWithError = Result.Failure(statusCode, errorMsg); + // Act var result = Result.PropagateError(resultWithError); + // Assert Assert.True(result.IsError); Assert.False(result.IsSuccess); Assert.Equal(errorMsg, result.ErrorMsg); @@ -51,12 +62,15 @@ public void PropagateError_WhenResultWithErrorPassed_ShouldReturnResultWithError [Fact] public void PropagateError_WhenNonGenericResultWithErrorPassed_ShouldReturnResultWithError() { + // Arrange const int statusCode = 500; const string errorMsg = "error message"; var resultWithError = Result.Failure(statusCode, errorMsg); + // Act var result = Result.PropagateError(resultWithError); + // Assert Assert.True(result.IsError); Assert.False(result.IsSuccess); Assert.Equal(errorMsg, result.ErrorMsg); @@ -66,20 +80,91 @@ public void PropagateError_WhenNonGenericResultWithErrorPassed_ShouldReturnResul [Fact] public void PropagateError_WhenResultWithSuccessPassed_ShouldThrowArgumentException() { + // Arrange var resultWithSuccess = Result.Success("abc"); + // Act var act = () => Result.PropagateError(resultWithSuccess); + // Assert Assert.Throws(act); } [Fact] public void PropagateError_WhenNonGenericResultWithSuccessPassed_ShouldThrowArgumentException() { + // Arrange var resultWithSuccess = Result.Success(); + // Act var act = () => Result.PropagateError(resultWithSuccess); + // Assert Assert.Throws(act); } + + [Fact] + public void ToObjectResult_WhenGenericResultIsError_ShouldReturnObjectResultWithErrorDetails() + { + // Arrange + const int statusCode = 404; + const string errorMsg = "Not found"; + var result = Result.Failure(statusCode, errorMsg); + + // Act + var objectResult = result.ToObjectResult(); + + // Assert + Assert.IsType(objectResult); + Assert.Equal(statusCode, objectResult.StatusCode); + Assert.Equal(errorMsg, objectResult.Value); + } + + [Fact] + public void ToObjectResult_WhenGenericResultIsSuccess_ShouldReturnObjectResultWithDefaultSuccessCode() + { + // Arrange + const int value = 42; + var result = Result.Success(value); + + // Act + var objectResult = result.ToObjectResult(); + + // Assert + Assert.IsType(objectResult); + Assert.Equal(StatusCodes.Status200OK, objectResult.StatusCode); + Assert.Equal(value, objectResult.Value); + } + + [Fact] + public void ToObjectResult_WhenGenericResultIsSuccessWithCustomStatusCode_ShouldReturnObjectResultWithSpecifiedCode() + { + // Arrange + const string value = "Success data"; + var result = Result.Success(value); + const int customSuccessCode = StatusCodes.Status201Created; + + // Act + var objectResult = result.ToObjectResult(customSuccessCode); + + // Assert + Assert.IsType(objectResult); + Assert.Equal(customSuccessCode, objectResult.StatusCode); + Assert.Equal(value, objectResult.Value); + } + + [Fact] + public void ToObjectResult_WhenGenericResultWithNullValueIsSuccess_ShouldReturnObjectResultWithNullValue() + { + // Arrange + var result = Result.Success(null); + + // Act + var objectResult = result.ToObjectResult(); + + // Assert + Assert.IsType(objectResult); + Assert.Equal(StatusCodes.Status200OK, objectResult.StatusCode); + Assert.Null(objectResult.Value); + } } \ No newline at end of file diff --git a/TickAPI/TickAPI.Tests/Common/Results/ResultTests.cs b/TickAPI/TickAPI.Tests/Common/Results/ResultTests.cs index f09668d..36799c1 100644 --- a/TickAPI/TickAPI.Tests/Common/Results/ResultTests.cs +++ b/TickAPI/TickAPI.Tests/Common/Results/ResultTests.cs @@ -1,4 +1,6 @@ -using TickAPI.Common.Results; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using TickAPI.Common.Results; using TickAPI.Common.Results.Generic; namespace TickAPI.Tests.Common.Results; @@ -8,8 +10,10 @@ public class ResultTests [Fact] public void Success_ShouldReturnResultWithSuccess() { + // Act var result = Result.Success(); + // Assert Assert.True(result.IsSuccess); Assert.False(result.IsError); Assert.Equal("", result.ErrorMsg); @@ -19,11 +23,14 @@ public void Success_ShouldReturnResultWithSuccess() [Fact] public void Failure_ShouldReturnResultWithError() { + // Arrange const int statusCode = 500; const string errorMsg = "example error msg"; + // Act var result = Result.Failure(500, errorMsg); + // Assert Assert.True(result.IsError); Assert.False(result.IsSuccess); Assert.Equal(errorMsg, result.ErrorMsg); @@ -33,12 +40,15 @@ public void Failure_ShouldReturnResultWithError() [Fact] public void PropagateError_WhenGenericResultWithErrorPassed_ShouldReturnResultWithError() { + // Arrange const int statusCode = 500; const string errorMsg = "error message"; var resultWithError = Result.Failure(statusCode, errorMsg); + // Act var result = Result.PropagateError(resultWithError); + // Assert Assert.True(result.IsError); Assert.False(result.IsSuccess); Assert.Equal(errorMsg, result.ErrorMsg); @@ -48,10 +58,61 @@ public void PropagateError_WhenGenericResultWithErrorPassed_ShouldReturnResultWi [Fact] public void PropagateError_WhenGenericResultWithSuccessPassed_ShouldThrowArgumentException() { + // Arrange var resultWithSuccess = Result.Success(123); + // Act var act = () => Result.PropagateError(resultWithSuccess); + // Assert Assert.Throws(act); } + + [Fact] + public void ToObjectResult_WhenResultIsError_ShouldReturnObjectResultWithErrorDetails() + { + // Arrange + const int statusCode = 400; + const string errorMsg = "Bad request"; + var result = Result.Failure(statusCode, errorMsg); + + // Act + var objectResult = result.ToObjectResult(); + + // Assert + Assert.IsType(objectResult); + Assert.Equal(statusCode, objectResult.StatusCode); + Assert.Equal(errorMsg, objectResult.Value); + } + + [Fact] + public void ToObjectResult_WhenResultIsSuccess_ShouldReturnObjectResultWithDefaultSuccessCode() + { + // Arrange + var result = Result.Success(); + + // Act + var objectResult = result.ToObjectResult(); + + // Assert + Assert.IsType(objectResult); + Assert.Equal(StatusCodes.Status200OK, objectResult.StatusCode); + Assert.Null(objectResult.Value); + } + + [Fact] + public void ToObjectResult_WhenResultIsSuccessWithCustomStatusCode_ShouldReturnObjectResultWithSpecifiedCode() + { + // Arrange + var result = Result.Success(); + const int customSuccessCode = StatusCodes.Status201Created; + + // Act + var objectResult = result.ToObjectResult(customSuccessCode); + + // Assert + Assert.IsType(objectResult); + Assert.Equal(customSuccessCode, objectResult.StatusCode); + Assert.Null(objectResult.Value); + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Results/Generic/Result.cs b/TickAPI/TickAPI/Common/Results/Generic/Result.cs index 10ccf62..b7e419e 100644 --- a/TickAPI/TickAPI/Common/Results/Generic/Result.cs +++ b/TickAPI/TickAPI/Common/Results/Generic/Result.cs @@ -1,4 +1,6 @@ -namespace TickAPI.Common.Results.Generic; +using Microsoft.AspNetCore.Mvc; + +namespace TickAPI.Common.Results.Generic; public record Result { @@ -45,4 +47,20 @@ public static Result PropagateError(Result other) return Failure(other.StatusCode, other.ErrorMsg); } + + public ObjectResult ToObjectResult(int successCode = StatusCodes.Status200OK) + { + if (IsError) + { + return new ObjectResult(ErrorMsg) + { + StatusCode = StatusCode + }; + } + + return new ObjectResult(Value) + { + StatusCode = successCode + }; + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Results/Result.cs b/TickAPI/TickAPI/Common/Results/Result.cs index d339858..08bc55e 100644 --- a/TickAPI/TickAPI/Common/Results/Result.cs +++ b/TickAPI/TickAPI/Common/Results/Result.cs @@ -1,4 +1,5 @@ -using TickAPI.Common.Results.Generic; +using Microsoft.AspNetCore.Mvc; +using TickAPI.Common.Results.Generic; namespace TickAPI.Common.Results; @@ -35,4 +36,20 @@ public static Result PropagateError(Result other) return Failure(other.StatusCode, other.ErrorMsg); } + + public ObjectResult ToObjectResult(int successCode = StatusCodes.Status200OK) + { + if (IsError) + { + return new ObjectResult(ErrorMsg) + { + StatusCode = StatusCode + }; + } + + return new ObjectResult(null) + { + StatusCode = successCode + }; + } } \ No newline at end of file From c0acb7d0880161acc45901be0a817583f4fa0532 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Sun, 4 May 2025 22:29:06 +0200 Subject: [PATCH 25/80] Simplify code using `ToObjectResult` --- .../Admins/Controllers/AdminsController.cs | 13 +++--- .../Controllers/CategoriesController.cs | 10 ++-- .../Controllers/CustomersController.cs | 10 ++-- .../Events/Controllers/EventsController.cs | 46 +++++-------------- .../Controllers/OrganizersController.cs | 34 ++++++-------- .../Tickets/Controllers/TicketsController.cs | 22 ++------- 6 files changed, 46 insertions(+), 89 deletions(-) diff --git a/TickAPI/TickAPI/Admins/Controllers/AdminsController.cs b/TickAPI/TickAPI/Admins/Controllers/AdminsController.cs index e6d70c2..803991e 100644 --- a/TickAPI/TickAPI/Admins/Controllers/AdminsController.cs +++ b/TickAPI/TickAPI/Admins/Controllers/AdminsController.cs @@ -31,20 +31,20 @@ public AdminsController(IGoogleAuthService googleAuthService, IJwtService jwtSer public async Task> GoogleLogin([FromBody] GoogleAdminLoginDto request) { var userDataResult = await _googleAuthService.GetUserDataFromAccessToken(request.AccessToken); - if(userDataResult.IsError) - return StatusCode(userDataResult.StatusCode, userDataResult.ErrorMsg); + if (userDataResult.IsError) + return userDataResult.ToObjectResult(); var userData = userDataResult.Value!; var adminResult = await _adminService.GetAdminByEmailAsync(userData.Email); if (adminResult.IsError) { - return StatusCode(adminResult.StatusCode, adminResult.ErrorMsg); + return adminResult.ToObjectResult(); } var jwtTokenResult = _jwtService.GenerateJwtToken(userData.Email, UserRole.Admin); if (jwtTokenResult.IsError) - return StatusCode(jwtTokenResult.StatusCode, jwtTokenResult.ErrorMsg); + return jwtTokenResult.ToObjectResult(); return new ActionResult(new GoogleAdminLoginResponseDto(jwtTokenResult.Value!)); } @@ -56,7 +56,7 @@ public async Task> AboutMe() var emailResult = _claimsService.GetEmailFromClaims(User.Claims); if (emailResult.IsError) { - return StatusCode(emailResult.StatusCode, emailResult.ErrorMsg); + return emailResult.ToObjectResult(); } var email = emailResult.Value!; @@ -66,8 +66,7 @@ public async Task> AboutMe() "cannot find user with admin privilages in database for authorized admin request"); var admin = adminResult.Value!; - var aboutMeResponse = - new AboutMeAdminResponseDto(admin.Email, admin.Login); + var aboutMeResponse = new AboutMeAdminResponseDto(admin.Email, admin.Login); return new ActionResult(aboutMeResponse); } } diff --git a/TickAPI/TickAPI/Categories/Controllers/CategoriesController.cs b/TickAPI/TickAPI/Categories/Controllers/CategoriesController.cs index 8ac66a2..22ad3a4 100644 --- a/TickAPI/TickAPI/Categories/Controllers/CategoriesController.cs +++ b/TickAPI/TickAPI/Categories/Controllers/CategoriesController.cs @@ -24,11 +24,7 @@ public CategoriesController(ICategoryService categoryService) public async Task>> GetCategories([FromQuery] int pageSize, [FromQuery] int page) { var res = await _categoryService.GetCategoriesResponsesAsync(pageSize, page); - if (res.IsError) - { - return StatusCode(StatusCodes.Status500InternalServerError, res.ErrorMsg); - } - return Ok(res.Value); + return res.ToObjectResult(); } [AuthorizeWithPolicy(AuthPolicies.AdminPolicy)] @@ -37,8 +33,8 @@ public async Task CreateCategory([FromBody] CreateCategoryDto requ { var newCategoryResult = await _categoryService.CreateNewCategoryAsync(request.Name); - if(newCategoryResult.IsError) - return StatusCode(newCategoryResult.StatusCode, newCategoryResult.ErrorMsg); + if (newCategoryResult.IsError) + return newCategoryResult.ToObjectResult(); return Ok("category created successfully"); } diff --git a/TickAPI/TickAPI/Customers/Controllers/CustomersController.cs b/TickAPI/TickAPI/Customers/Controllers/CustomersController.cs index 4ec0730..0a77fea 100644 --- a/TickAPI/TickAPI/Customers/Controllers/CustomersController.cs +++ b/TickAPI/TickAPI/Customers/Controllers/CustomersController.cs @@ -30,8 +30,8 @@ public CustomersController(IGoogleAuthService googleAuthService, IJwtService jwt public async Task> GoogleLogin([FromBody] GoogleCustomerLoginDto request) { var userDataResult = await _googleAuthService.GetUserDataFromAccessToken(request.AccessToken); - if(userDataResult.IsError) - return StatusCode(userDataResult.StatusCode, userDataResult.ErrorMsg); + if (userDataResult.IsError) + return userDataResult.ToObjectResult(); var userData = userDataResult.Value!; @@ -40,12 +40,12 @@ public async Task> GoogleLogin([Fro { var newCustomerResult = await _customerService.CreateNewCustomerAsync(userData.Email, userData.GivenName, userData.FamilyName); if (newCustomerResult.IsError) - return StatusCode(newCustomerResult.StatusCode, newCustomerResult.ErrorMsg); + return newCustomerResult.ToObjectResult(); } var jwtTokenResult = _jwtService.GenerateJwtToken(userData.Email, UserRole.Customer); if (jwtTokenResult.IsError) - return StatusCode(jwtTokenResult.StatusCode, jwtTokenResult.ErrorMsg); + return jwtTokenResult.ToObjectResult(); return new ActionResult(new GoogleCustomerLoginResponseDto(jwtTokenResult.Value!)); } @@ -57,7 +57,7 @@ public async Task> AboutMe() var emailResult = _claimsService.GetEmailFromClaims(User.Claims); if (emailResult.IsError) { - return StatusCode(emailResult.StatusCode, emailResult.ErrorMsg); + return emailResult.ToObjectResult(); } var email = emailResult.Value!; diff --git a/TickAPI/TickAPI/Events/Controllers/EventsController.cs b/TickAPI/TickAPI/Events/Controllers/EventsController.cs index c5b3d09..c1ff0e1 100644 --- a/TickAPI/TickAPI/Events/Controllers/EventsController.cs +++ b/TickAPI/TickAPI/Events/Controllers/EventsController.cs @@ -33,16 +33,16 @@ public async Task> CreateEvent([FromBody] C var emailResult = _claimsService.GetEmailFromClaims(User.Claims); if (emailResult.IsError) { - return StatusCode(emailResult.StatusCode, emailResult.ErrorMsg); + return emailResult.ToObjectResult(); } var email = emailResult.Value!; var newEventResult = await _eventService.CreateNewEventAsync(request.Name, request.Description, request.StartDate, request.EndDate, request.MinimumAge, request.CreateAddress, request.Categories , request.TicketTypes ,request.EventStatus, email); - + if (newEventResult.IsError) - return StatusCode(newEventResult.StatusCode, newEventResult.ErrorMsg); + return newEventResult.ToObjectResult(); return Ok("Event created succesfully"); } @@ -54,24 +54,19 @@ public async Task>> GetOrganizer var emailResult = _claimsService.GetEmailFromClaims(User.Claims); if (emailResult.IsError) { - return StatusCode(emailResult.StatusCode, emailResult.ErrorMsg); + return emailResult.ToObjectResult(); } var email = emailResult.Value!; var organizerResult = await _organizerService.GetOrganizerByEmailAsync(email); if (organizerResult.IsError) { - return StatusCode(organizerResult.StatusCode, organizerResult.ErrorMsg); + return organizerResult.ToObjectResult(); } var organizer = organizerResult.Value!; var paginatedDataResult = await _eventService.GetOrganizerEventsAsync(organizer, page, pageSize, eventFilters); - if (paginatedDataResult.IsError) - { - return StatusCode(paginatedDataResult.StatusCode, paginatedDataResult.ErrorMsg); - } - - return Ok(paginatedDataResult.Value!); + return paginatedDataResult.ToObjectResult(); } [AuthorizeWithPolicy(AuthPolicies.VerifiedOrganizerPolicy)] @@ -81,24 +76,19 @@ public async Task> GetOrganizerEventsPaginationD var emailResult = _claimsService.GetEmailFromClaims(User.Claims); if (emailResult.IsError) { - return StatusCode(emailResult.StatusCode, emailResult.ErrorMsg); + return emailResult.ToObjectResult(); } var email = emailResult.Value!; var organizerResult = await _organizerService.GetOrganizerByEmailAsync(email); if (organizerResult.IsError) { - return StatusCode(organizerResult.StatusCode, organizerResult.ErrorMsg); + return organizerResult.ToObjectResult(); } var organizer = organizerResult.Value!; var paginationDetailsResult = await _eventService.GetOrganizerEventsPaginationDetailsAsync(organizer, pageSize); - if (paginationDetailsResult.IsError) - { - return StatusCode(paginationDetailsResult.StatusCode, paginationDetailsResult.ErrorMsg); - } - - return Ok(paginationDetailsResult.Value!); + return paginationDetailsResult.ToObjectResult(); } [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] @@ -106,11 +96,7 @@ public async Task> GetOrganizerEventsPaginationD public async Task>> GetEvents([FromQuery] int pageSize, [FromQuery] int page, [FromQuery] EventFiltersDto eventFilters) { var paginatedDataResult = await _eventService.GetEventsAsync(page, pageSize, eventFilters); - if (paginatedDataResult.IsError) - { - return StatusCode(paginatedDataResult.StatusCode, paginatedDataResult.ErrorMsg); - } - return Ok(paginatedDataResult.Value!); + return paginatedDataResult.ToObjectResult(); } [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] @@ -118,11 +104,7 @@ public async Task>> GetEvents([F public async Task> GetEventsPaginationDetails([FromQuery] int pageSize) { var paginationDetailsResult = await _eventService.GetEventsPaginationDetailsAsync(pageSize); - if (paginationDetailsResult.IsError) - { - return StatusCode(paginationDetailsResult.StatusCode, paginationDetailsResult.ErrorMsg); - } - return Ok(paginationDetailsResult.Value!); + return paginationDetailsResult.ToObjectResult(); } [AuthorizeWithPolicy(AuthPolicies.VerifiedUserPolicy)] @@ -130,10 +112,6 @@ public async Task> GetEventsPaginationDetails([F public async Task> GetEventDetails([FromRoute] Guid id) { var eventDetailsResult = await _eventService.GetEventDetailsAsync(id); - if (eventDetailsResult.IsError) - { - return StatusCode(eventDetailsResult.StatusCode, eventDetailsResult.ErrorMsg); - } - return Ok(eventDetailsResult.Value!); + return eventDetailsResult.ToObjectResult(); } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Organizers/Controllers/OrganizersController.cs b/TickAPI/TickAPI/Organizers/Controllers/OrganizersController.cs index f7f9a64..efecb40 100644 --- a/TickAPI/TickAPI/Organizers/Controllers/OrganizersController.cs +++ b/TickAPI/TickAPI/Organizers/Controllers/OrganizersController.cs @@ -32,8 +32,8 @@ public OrganizersController(IGoogleAuthService googleAuthService, IJwtService jw public async Task> GoogleLogin([FromBody] GoogleOrganizerLoginDto request) { var userDataResult = await _googleAuthService.GetUserDataFromAccessToken(request.AccessToken); - if(userDataResult.IsError) - return StatusCode(userDataResult.StatusCode, userDataResult.ErrorMsg); + if (userDataResult.IsError) + return userDataResult.ToObjectResult(); var userData = userDataResult.Value!; @@ -42,9 +42,9 @@ public async Task> GoogleLogin([Fr if (existingOrganizerResult.IsError) { jwtTokenResult = _jwtService.GenerateJwtToken(userData.Email, UserRole.NewOrganizer); - - if(jwtTokenResult.IsError) - return StatusCode(jwtTokenResult.StatusCode, jwtTokenResult.ErrorMsg); + + if (jwtTokenResult.IsError) + return jwtTokenResult.ToObjectResult(); return new ActionResult(new GoogleOrganizerLoginResponseDto(jwtTokenResult.Value!, true, false)); } @@ -54,9 +54,9 @@ public async Task> GoogleLogin([Fr var role = isVerified ? UserRole.Organizer : UserRole.UnverifiedOrganizer; jwtTokenResult = _jwtService.GenerateJwtToken(userData.Email, role); - - if(jwtTokenResult.IsError) - return StatusCode(jwtTokenResult.StatusCode, jwtTokenResult.ErrorMsg); + + if (jwtTokenResult.IsError) + return jwtTokenResult.ToObjectResult(); return new ActionResult(new GoogleOrganizerLoginResponseDto(jwtTokenResult.Value!, false, isVerified)); } @@ -68,17 +68,17 @@ public async Task> CreateOrganizer([Fro var emailResult = _claimsService.GetEmailFromClaims(User.Claims); if (emailResult.IsError) { - return StatusCode(StatusCodes.Status400BadRequest, emailResult.ErrorMsg); + return emailResult.ToObjectResult(); } var email = emailResult.Value!; var newOrganizerResult = await _organizerService.CreateNewOrganizerAsync(email, request.FirstName, request.LastName, request.DisplayName); - if(newOrganizerResult.IsError) - return StatusCode(newOrganizerResult.StatusCode, newOrganizerResult.ErrorMsg); + if (newOrganizerResult.IsError) + return newOrganizerResult.ToObjectResult(); var jwtTokenResult = _jwtService.GenerateJwtToken(newOrganizerResult.Value!.Email, newOrganizerResult.Value!.IsVerified ? UserRole.Organizer : UserRole.UnverifiedOrganizer); - if(jwtTokenResult.IsError) - return StatusCode(jwtTokenResult.StatusCode, jwtTokenResult.ErrorMsg); + if (jwtTokenResult.IsError) + return jwtTokenResult.ToObjectResult(); return new ActionResult(new CreateOrganizerResponseDto(jwtTokenResult.Value!)); } @@ -88,11 +88,7 @@ public async Task> CreateOrganizer([Fro public async Task VerifyOrganizer([FromBody] VerifyOrganizerDto request) { var verifyOrganizerResult = await _organizerService.VerifyOrganizerByEmailAsync(request.Email); - - if(verifyOrganizerResult.IsError) - return StatusCode(verifyOrganizerResult.StatusCode, verifyOrganizerResult.ErrorMsg); - - return Ok(); + return verifyOrganizerResult.ToObjectResult(); } [AuthorizeWithPolicy(AuthPolicies.CreatedOrganizerPolicy)] @@ -102,7 +98,7 @@ public async Task> AboutMe() var emailResult = _claimsService.GetEmailFromClaims(User.Claims); if (emailResult.IsError) { - return StatusCode(StatusCodes.Status400BadRequest, emailResult.ErrorMsg); + return emailResult.ToObjectResult(); } var email = emailResult.Value!; diff --git a/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs b/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs index 78979fb..f5148c9 100644 --- a/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs +++ b/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs @@ -27,26 +27,18 @@ public async Task> GetTicketDetails(Gu var emailResult = _claimsService.GetEmailFromClaims(User.Claims); if (emailResult.IsError) { - return StatusCode(emailResult.StatusCode, emailResult.ErrorMsg); + return emailResult.ToObjectResult(); } var email = emailResult.Value!; var ticket = await _ticketService.GetTicketDetailsAsync(id, email); - if (ticket.IsError) - { - return StatusCode(ticket.StatusCode, ticket.ErrorMsg); - } - return Ok(ticket.Value); + return ticket.ToObjectResult(); } [HttpGet("/for-resell")] public async Task>> GetTicketsForResell([FromQuery] Guid eventId, [FromQuery] int pageSize, [FromQuery] int page) { var result = await _ticketService.GetTicketsForResellAsync(eventId, page, pageSize); - if (result.IsError) - { - return StatusCode(result.StatusCode, result.ErrorMsg); - } - return result.Value!; + return result.ToObjectResult(); } [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] @@ -56,13 +48,9 @@ public async Task>> GetTicke var emailResult = _claimsService.GetEmailFromClaims(User.Claims); if (emailResult.IsError) { - return StatusCode(emailResult.StatusCode, emailResult.ErrorMsg); + return emailResult.ToObjectResult(); } var tickets = await _ticketService.GetTicketsForCustomerAsync(emailResult.Value!, page, pageSize); - if (tickets.IsError) - { - return StatusCode(tickets.StatusCode, tickets.ErrorMsg); - } - return Ok(tickets.Value); + return tickets.ToObjectResult(); } } \ No newline at end of file From 19e1cd4e53c2d90eaac3f61f8c5bdf4f0a9c34e5 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Thu, 8 May 2025 22:52:03 +0200 Subject: [PATCH 26/80] Use more generic `ObjectResult` instead of `OkObjectResult` in tests --- .../Controllers/CategoriesControllerTests.cs | 2 +- .../Events/Controllers/EventsControllerTests.cs | 10 +++++----- .../Controllers/OrganizersControllerTests.cs | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/TickAPI/TickAPI.Tests/Categories/Controllers/CategoriesControllerTests.cs b/TickAPI/TickAPI.Tests/Categories/Controllers/CategoriesControllerTests.cs index 271fd24..79283fd 100644 --- a/TickAPI/TickAPI.Tests/Categories/Controllers/CategoriesControllerTests.cs +++ b/TickAPI/TickAPI.Tests/Categories/Controllers/CategoriesControllerTests.cs @@ -32,7 +32,7 @@ public async Task GetCategories_WhenDataIsValid_ShouldReturnOk() // Assert var result = Assert.IsType>>(res); - var objectResult = Assert.IsType(result.Result); + var objectResult = Assert.IsType(result.Result); Assert.Equal(200, objectResult.StatusCode); Assert.NotNull(objectResult.Value); } diff --git a/TickAPI/TickAPI.Tests/Events/Controllers/EventsControllerTests.cs b/TickAPI/TickAPI.Tests/Events/Controllers/EventsControllerTests.cs index 60e6cc7..36edd57 100644 --- a/TickAPI/TickAPI.Tests/Events/Controllers/EventsControllerTests.cs +++ b/TickAPI/TickAPI.Tests/Events/Controllers/EventsControllerTests.cs @@ -191,7 +191,7 @@ public async Task GetOrganizerEvents_WhenAllOperationsSucceed_ShouldReturnOkWith // Assert var result = Assert.IsType>>(response); - var okResult = Assert.IsType(result.Result); + var okResult = Assert.IsType(result.Result); Assert.Equal(StatusCodes.Status200OK, okResult.StatusCode); var returnedPaginatedData = Assert.IsType>(okResult.Value); @@ -385,7 +385,7 @@ public async Task GetOrganizerEventsPaginationDetails_WhenAllOperationsSucceed_S // Assert var result = Assert.IsType>(response); - var okResult = Assert.IsType(result.Result); + var okResult = Assert.IsType(result.Result); Assert.Equal(StatusCodes.Status200OK, okResult.StatusCode); var returnedPaginationDetails = Assert.IsType(okResult.Value); @@ -479,7 +479,7 @@ public async Task GetEvents_WhenAllOperationsSucceed_ShouldReturnOkWithPaginated // Assert var result = Assert.IsType>>(response); - var okResult = Assert.IsType(result.Result); + var okResult = Assert.IsType(result.Result); Assert.Equal(StatusCodes.Status200OK, okResult.StatusCode); var returnedPaginatedData = Assert.IsType>(okResult.Value); @@ -544,7 +544,7 @@ public async Task GetEventsPaginationDetails_WhenAllOperationsSucceed_ShouldRetu // Assert var result = Assert.IsType>(response); - var okResult = Assert.IsType(result.Result); + var okResult = Assert.IsType(result.Result); Assert.Equal(StatusCodes.Status200OK, okResult.StatusCode); var returnedPaginationDetails = Assert.IsType(okResult.Value); @@ -602,7 +602,7 @@ public async Task GetEventDetails_WhenAllOperationsSucceed_ShouldReturnOkWithEve // Assert var result = Assert.IsType>(response); - var okResult = Assert.IsType(result.Result); + var okResult = Assert.IsType(result.Result); Assert.Equal(StatusCodes.Status200OK, okResult.StatusCode); var returnedEventDetails = Assert.IsType(okResult.Value); diff --git a/TickAPI/TickAPI.Tests/Organizers/Controllers/OrganizersControllerTests.cs b/TickAPI/TickAPI.Tests/Organizers/Controllers/OrganizersControllerTests.cs index 164b26c..21f8348 100644 --- a/TickAPI/TickAPI.Tests/Organizers/Controllers/OrganizersControllerTests.cs +++ b/TickAPI/TickAPI.Tests/Organizers/Controllers/OrganizersControllerTests.cs @@ -268,7 +268,7 @@ public async Task VerifyOrganizer_WhenVerificationSuccessful_ShouldReturnOk() var actionResult = await sut.VerifyOrganizer(new VerifyOrganizerDto(email)); // Assert - var result = Assert.IsType(actionResult); + var result = Assert.IsType(actionResult); Assert.Equal(StatusCodes.Status200OK, result.StatusCode); } From ef850db9dea72f90f58719edbd821b48e83b8ccb Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Thu, 8 May 2025 23:10:00 +0200 Subject: [PATCH 27/80] Add support for missing fields in `EventFiltersDto` --- .../Controllers/EventsControllerTests.cs | 2 +- .../Events/Filters/EventFilterApplierTests.cs | 145 +++++++++++++++--- .../Events/DTOs/Request/EventFiltersDto.cs | 4 +- .../Events/Filters/EventFilterApplier.cs | 4 +- 4 files changed, 133 insertions(+), 22 deletions(-) diff --git a/TickAPI/TickAPI.Tests/Events/Controllers/EventsControllerTests.cs b/TickAPI/TickAPI.Tests/Events/Controllers/EventsControllerTests.cs index 60e6cc7..69b4f4e 100644 --- a/TickAPI/TickAPI.Tests/Events/Controllers/EventsControllerTests.cs +++ b/TickAPI/TickAPI.Tests/Events/Controllers/EventsControllerTests.cs @@ -20,7 +20,7 @@ namespace TickAPI.Tests.Events.Controllers; public class EventsControllerTests { - private readonly EventFiltersDto _emptyFilters = new EventFiltersDto(null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null); + private readonly EventFiltersDto _emptyFilters = new EventFiltersDto(null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null); [Fact] public async Task CreateEvent_WhenDataIsValid_ShouldReturnSuccess() diff --git a/TickAPI/TickAPI.Tests/Events/Filters/EventFilterApplierTests.cs b/TickAPI/TickAPI.Tests/Events/Filters/EventFilterApplierTests.cs index 7657c21..b836508 100644 --- a/TickAPI/TickAPI.Tests/Events/Filters/EventFilterApplierTests.cs +++ b/TickAPI/TickAPI.Tests/Events/Filters/EventFilterApplierTests.cs @@ -40,7 +40,9 @@ public void ApplyFilters_WithName_ShouldCallFilterByName() AddressCity: null, AddressStreet: null, HouseNumber: null, - FlatNumber: null + FlatNumber: null, + PostalCode: null, + CategoriesNames: null ); // Act @@ -72,7 +74,9 @@ public void ApplyFilters_WithDescription_ShouldCallFilterByDescription() AddressCity: null, AddressStreet: null, HouseNumber: null, - FlatNumber: null + FlatNumber: null, + PostalCode: null, + CategoriesNames: null ); // Act @@ -105,7 +109,9 @@ public void ApplyFilters_WithStartDate_ShouldCallFilterByStartDate() AddressCity: null, AddressStreet: null, HouseNumber: null, - FlatNumber: null + FlatNumber: null, + PostalCode: null, + CategoriesNames: null ); // Act @@ -138,7 +144,9 @@ public void ApplyFilters_WithMinStartDate_ShouldCallFilterByMinStartDate() AddressCity: null, AddressStreet: null, HouseNumber: null, - FlatNumber: null + FlatNumber: null, + PostalCode: null, + CategoriesNames: null ); // Act @@ -171,7 +179,9 @@ public void ApplyFilters_WithMaxStartDate_ShouldCallFilterByMaxStartDate() AddressCity: null, AddressStreet: null, HouseNumber: null, - FlatNumber: null + FlatNumber: null, + PostalCode: null, + CategoriesNames: null ); // Act @@ -204,7 +214,9 @@ public void ApplyFilters_WithEndDate_ShouldCallFilterByEndDate() AddressCity: null, AddressStreet: null, HouseNumber: null, - FlatNumber: null + FlatNumber: null, + PostalCode: null, + CategoriesNames: null ); // Act @@ -237,7 +249,9 @@ public void ApplyFilters_WithMinEndDate_ShouldCallFilterByMinEndDate() AddressCity: null, AddressStreet: null, HouseNumber: null, - FlatNumber: null + FlatNumber: null, + PostalCode: null, + CategoriesNames: null ); // Act @@ -270,7 +284,9 @@ public void ApplyFilters_WithMaxEndDate_ShouldCallFilterByMaxEndDate() AddressCity: null, AddressStreet: null, HouseNumber: null, - FlatNumber: null + FlatNumber: null, + PostalCode: null, + CategoriesNames: null ); // Act @@ -303,7 +319,9 @@ public void ApplyFilters_WithMinPrice_ShouldCallFilterByMinPrice() AddressCity: null, AddressStreet: null, HouseNumber: null, - FlatNumber: null + FlatNumber: null, + PostalCode: null, + CategoriesNames: null ); // Act @@ -336,7 +354,9 @@ public void ApplyFilters_WithMaxPrice_ShouldCallFilterByMaxPrice() AddressCity: null, AddressStreet: null, HouseNumber: null, - FlatNumber: null + FlatNumber: null, + PostalCode: null, + CategoriesNames: null ); // Act @@ -369,7 +389,9 @@ public void ApplyFilters_WithMinAge_ShouldCallFilterByMinAge() AddressCity: null, AddressStreet: null, HouseNumber: null, - FlatNumber: null + FlatNumber: null, + PostalCode: null, + CategoriesNames: null ); // Act @@ -402,7 +424,9 @@ public void ApplyFilters_WithMaxMinimumAge_ShouldCallFilterByMaxMinimumAge() AddressCity: null, AddressStreet: null, HouseNumber: null, - FlatNumber: null + FlatNumber: null, + PostalCode: null, + CategoriesNames: null ); // Act @@ -434,7 +458,9 @@ public void ApplyFilters_WithAddressCountry_ShouldCallFilterByAddressCountry() AddressCity: null, AddressStreet: null, HouseNumber: null, - FlatNumber: null + FlatNumber: null, + PostalCode: null, + CategoriesNames: null ); // Act @@ -466,7 +492,9 @@ public void ApplyFilters_WithAddressCity_ShouldCallFilterByAddressCity() AddressCity: "Warsaw", AddressStreet: null, HouseNumber: null, - FlatNumber: null + FlatNumber: null, + PostalCode: null, + CategoriesNames: null ); // Act @@ -498,7 +526,9 @@ public void ApplyFilters_WithAddressStreetOnly_ShouldCallFilterByAddressStreet() AddressCity: null, AddressStreet: "Marszałkowska", HouseNumber: null, - FlatNumber: null + FlatNumber: null, + PostalCode: null, + CategoriesNames: null ); // Act @@ -531,7 +561,9 @@ public void ApplyFilters_WithAddressStreetAndHouseNumber_ShouldCallFilterByAddre AddressCity: null, AddressStreet: "Marszałkowska", HouseNumber: houseNumber, - FlatNumber: null + FlatNumber: null, + PostalCode: null, + CategoriesNames: null ); // Act @@ -565,7 +597,9 @@ public void ApplyFilters_WithCompleteAddress_ShouldCallFilterByAddressStreet() AddressCity: null, AddressStreet: "Marszałkowska", HouseNumber: houseNumber, - FlatNumber: flatNumber + FlatNumber: flatNumber, + PostalCode: null, + CategoriesNames: null ); // Act @@ -575,6 +609,75 @@ public void ApplyFilters_WithCompleteAddress_ShouldCallFilterByAddressStreet() _mockEventFilter.Verify(ef => ef.FilterByAddressStreet(filters.AddressStreet!, filters.HouseNumber, filters.FlatNumber), Times.Once); _mockEventFilter.Verify(ef => ef.GetEvents(), Times.Once); } + + [Fact] + public void ApplyFilters_WithPostalCode_ShouldCallFilterByAddressPostalCode() + { + // Arrange + var filters = new EventFiltersDto( + Name: null, + Descritpion: null, + StartDate: null, + MinStartDate: null, + MaxStartDate: null, + EndDate: null, + MinEndDate: null, + MaxEndDate: null, + MinPrice: null, + MaxPrice: null, + MinAge: null, + MaxMinimumAge: null, + AddressCountry: null, + AddressCity: null, + AddressStreet: null, + HouseNumber: null, + FlatNumber: null, + PostalCode: "00-001", + CategoriesNames: null + ); + + // Act + _eventFilterApplier.ApplyFilters(filters); + + // Assert + _mockEventFilter.Verify(ef => ef.FilterByAddressPostalCode(filters.PostalCode!), Times.Once); + _mockEventFilter.Verify(ef => ef.GetEvents(), Times.Once); + } + + [Fact] + public void ApplyFilters_WithCategoriesNames_ShouldCallFilterByCategoriesNames() + { + // Arrange + var categoriesNames = new List { "Concert", "Festival", "Exhibition" }; + var filters = new EventFiltersDto( + Name: null, + Descritpion: null, + StartDate: null, + MinStartDate: null, + MaxStartDate: null, + EndDate: null, + MinEndDate: null, + MaxEndDate: null, + MinPrice: null, + MaxPrice: null, + MinAge: null, + MaxMinimumAge: null, + AddressCountry: null, + AddressCity: null, + AddressStreet: null, + HouseNumber: null, + FlatNumber: null, + PostalCode: null, + CategoriesNames: categoriesNames + ); + + // Act + _eventFilterApplier.ApplyFilters(filters); + + // Assert + _mockEventFilter.Verify(ef => ef.FilterByCategoriesNames(filters.CategoriesNames!), Times.Once); + _mockEventFilter.Verify(ef => ef.GetEvents(), Times.Once); + } [Fact] public void ApplyFilters_WithMultipleFilters_ShouldCallAllRelevantFilters() @@ -600,7 +703,9 @@ public void ApplyFilters_WithMultipleFilters_ShouldCallAllRelevantFilters() AddressCity: null, AddressStreet: null, HouseNumber: null, - FlatNumber: null + FlatNumber: null, + PostalCode: null, + CategoriesNames: null ); // Act @@ -641,7 +746,9 @@ public void ApplyFilters_WithNoFilters_ShouldOnlyCallGetEvents() AddressCity: null, AddressStreet: null, HouseNumber: null, - FlatNumber: null + FlatNumber: null, + PostalCode: null, + CategoriesNames: null ); // Act diff --git a/TickAPI/TickAPI/Events/DTOs/Request/EventFiltersDto.cs b/TickAPI/TickAPI/Events/DTOs/Request/EventFiltersDto.cs index 91e52cc..7eba343 100644 --- a/TickAPI/TickAPI/Events/DTOs/Request/EventFiltersDto.cs +++ b/TickAPI/TickAPI/Events/DTOs/Request/EventFiltersDto.cs @@ -17,5 +17,7 @@ public record EventFiltersDto( string? AddressCity, string? AddressStreet, uint? HouseNumber, - uint? FlatNumber + uint? FlatNumber, + string? PostalCode, + List? CategoriesNames ); diff --git a/TickAPI/TickAPI/Events/Filters/EventFilterApplier.cs b/TickAPI/TickAPI/Events/Filters/EventFilterApplier.cs index 7347465..a9f890b 100644 --- a/TickAPI/TickAPI/Events/Filters/EventFilterApplier.cs +++ b/TickAPI/TickAPI/Events/Filters/EventFilterApplier.cs @@ -31,7 +31,9 @@ public EventFilterApplier(IEventFilter eventFilter) { f => !string.IsNullOrEmpty(f.AddressStreet), f => _eventFilter.FilterByAddressStreet( f.AddressStreet!, f.HouseNumber, - f.FlatNumber) } + f.FlatNumber) }, + {f => !string.IsNullOrEmpty(f.PostalCode), f => _eventFilter.FilterByAddressPostalCode(f.PostalCode!)}, + {f => f.CategoriesNames is { Count: > 0 }, f => _eventFilter.FilterByCategoriesNames(f.CategoriesNames!)} }; } From 066d92fdbf6ba977e496528027355943a1c9bc35 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Thu, 8 May 2025 23:19:58 +0200 Subject: [PATCH 28/80] Add currency to the data returned while getting events --- TickAPI/TickAPI.Tests/Events/Utils.cs | 4 ++-- .../TickAPI/Events/DTOs/Response/GetEventResponseDto.cs | 4 ++-- .../Events/DTOs/Response/GetEventResponsePriceInfoDto.cs | 6 ++++++ TickAPI/TickAPI/Events/Services/EventService.cs | 8 ++++++-- 4 files changed, 16 insertions(+), 6 deletions(-) create mode 100644 TickAPI/TickAPI/Events/DTOs/Response/GetEventResponsePriceInfoDto.cs diff --git a/TickAPI/TickAPI.Tests/Events/Utils.cs b/TickAPI/TickAPI.Tests/Events/Utils.cs index 479568b..1fa0c76 100644 --- a/TickAPI/TickAPI.Tests/Events/Utils.cs +++ b/TickAPI/TickAPI.Tests/Events/Utils.cs @@ -47,8 +47,8 @@ public static GetEventResponseDto CreateSampleEventResponseDto(string name) new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc), new DateTime(1970, 1, 2, 0, 0, 0, DateTimeKind.Utc), 18, - 100, - 300, + new GetEventResponsePriceInfoDto(100, "PLN"), + new GetEventResponsePriceInfoDto(300, "PLN"), [new GetEventResponseCategoryDto("Test")], EventStatus.TicketsAvailable, new GetEventResponseAddressDto("United States", "New York", "10001", "Main St", 123, null) diff --git a/TickAPI/TickAPI/Events/DTOs/Response/GetEventResponseDto.cs b/TickAPI/TickAPI/Events/DTOs/Response/GetEventResponseDto.cs index 28b42e4..a280251 100644 --- a/TickAPI/TickAPI/Events/DTOs/Response/GetEventResponseDto.cs +++ b/TickAPI/TickAPI/Events/DTOs/Response/GetEventResponseDto.cs @@ -9,8 +9,8 @@ public record GetEventResponseDto( DateTime StartDate, DateTime EndDate, uint? MinimumAge, - decimal MinimumPrice, - decimal MaximumPrice, + GetEventResponsePriceInfoDto MinimumPrice, + GetEventResponsePriceInfoDto MaximumPrice, List Categories, EventStatus Status, GetEventResponseAddressDto Address diff --git a/TickAPI/TickAPI/Events/DTOs/Response/GetEventResponsePriceInfoDto.cs b/TickAPI/TickAPI/Events/DTOs/Response/GetEventResponsePriceInfoDto.cs new file mode 100644 index 0000000..6c321a7 --- /dev/null +++ b/TickAPI/TickAPI/Events/DTOs/Response/GetEventResponsePriceInfoDto.cs @@ -0,0 +1,6 @@ +namespace TickAPI.Events.DTOs.Response; + +public record GetEventResponsePriceInfoDto( + decimal Price, + string Currency +); diff --git a/TickAPI/TickAPI/Events/Services/EventService.cs b/TickAPI/TickAPI/Events/Services/EventService.cs index 201a9a9..8177bd7 100644 --- a/TickAPI/TickAPI/Events/Services/EventService.cs +++ b/TickAPI/TickAPI/Events/Services/EventService.cs @@ -193,8 +193,12 @@ private static GetEventResponseDto MapEventToGetEventResponseDto(Event ev) var categories = ev.Categories.Count > 0 ? ev.Categories.Select((c) => new GetEventResponseCategoryDto(c.Name)).ToList() : new List(); var address = new GetEventResponseAddressDto(ev.Address.Country, ev.Address.City, ev.Address.PostalCode, ev.Address.Street, ev.Address.HouseNumber, ev.Address.FlatNumber); - var minimumPrice = ev.TicketTypes.Min(t => t.Price); - var maximumPrice = ev.TicketTypes.Max(t => t.Price); + // Here we assume that there is at least one ticket type in each event + var ttMinimumPrice = ev.TicketTypes.MinBy(t => t.Price)!; + var ttMaximumPrice = ev.TicketTypes.MaxBy(t => t.Price)!; + + var minimumPrice = new GetEventResponsePriceInfoDto(ttMinimumPrice.Price, ttMinimumPrice.Currency); + var maximumPrice = new GetEventResponsePriceInfoDto(ttMaximumPrice.Price, ttMaximumPrice.Currency); return new GetEventResponseDto(ev.Id, ev.Name, ev.Description, ev.StartDate, ev.EndDate, ev.MinimumAge, minimumPrice, maximumPrice, categories, ev.EventStatus, address); From fb02eb58a3aeb3b78d36462080090fe84ca8088d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Fri, 9 May 2025 07:03:25 +0200 Subject: [PATCH 29/80] Working feature --- .../Tickets/Services/TicketServiceTests.cs | 6 +- .../20250508114210_UsedTicket.Designer.cs | 392 ++++++++++++++++++ .../Migrations/20250508114210_UsedTicket.cs | 29 ++ .../TickApiDbContextModelSnapshot.cs | 3 + .../Tickets/Abstractions/ITicketFilter.cs | 13 + .../Abstractions/ITicketFilterApplier.cs | 9 + .../Tickets/Abstractions/ITicketService.cs | 3 +- .../Tickets/Controllers/TicketsController.cs | 6 +- .../Tickets/DTOs/Request/TicketFiltersDto.cs | 10 + .../DTOs/Response/GetTicketForCustomerDto.cs | 1 + .../TickAPI/Tickets/Filters/TicketFilter.cs | 44 ++ .../Tickets/Filters/TicketFilterApplier.cs | 37 ++ TickAPI/TickAPI/Tickets/Models/Ticket.cs | 1 + .../TickAPI/Tickets/Services/TicketService.cs | 12 +- 14 files changed, 559 insertions(+), 7 deletions(-) create mode 100644 TickAPI/TickAPI/Migrations/20250508114210_UsedTicket.Designer.cs create mode 100644 TickAPI/TickAPI/Migrations/20250508114210_UsedTicket.cs create mode 100644 TickAPI/TickAPI/Tickets/Abstractions/ITicketFilter.cs create mode 100644 TickAPI/TickAPI/Tickets/Abstractions/ITicketFilterApplier.cs create mode 100644 TickAPI/TickAPI/Tickets/DTOs/Request/TicketFiltersDto.cs create mode 100644 TickAPI/TickAPI/Tickets/Filters/TicketFilter.cs create mode 100644 TickAPI/TickAPI/Tickets/Filters/TicketFilterApplier.cs diff --git a/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs b/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs index 479c287..6ff2147 100644 --- a/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs @@ -447,6 +447,7 @@ public async Task GetTicketsForCustomerAsync_WithValidInput_ReturnsSuccessResult { new Ticket { + Id = new Guid(), Type = new TicketType { Event = new Event @@ -459,6 +460,7 @@ public async Task GetTicketsForCustomerAsync_WithValidInput_ReturnsSuccessResult }, new Ticket { + Id = new Guid(), Type = new TicketType { Event = new Event @@ -480,8 +482,8 @@ public async Task GetTicketsForCustomerAsync_WithValidInput_ReturnsSuccessResult false, new PaginationDetails(0, 2) ); - var mappedData1 = new GetTicketForCustomerDto("EventName", new DateTime(2025, 10, 10), new DateTime(2025, 10, 20)); - var mappedData2 = new GetTicketForCustomerDto("EventName2", new DateTime(2025, 11, 10), new DateTime(2025, 11, 20)); + var mappedData1 = new GetTicketForCustomerDto(tickets[0].Id, "EventName", new DateTime(2025, 10, 10), new DateTime(2025, 10, 20)); + var mappedData2 = new GetTicketForCustomerDto(tickets[1].Id, "EventName2", new DateTime(2025, 11, 10), new DateTime(2025, 11, 20)); var mappedPaginatedData = new PaginatedData ( new List{mappedData1, mappedData2}, diff --git a/TickAPI/TickAPI/Migrations/20250508114210_UsedTicket.Designer.cs b/TickAPI/TickAPI/Migrations/20250508114210_UsedTicket.Designer.cs new file mode 100644 index 0000000..f2dca06 --- /dev/null +++ b/TickAPI/TickAPI/Migrations/20250508114210_UsedTicket.Designer.cs @@ -0,0 +1,392 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using TickAPI.Common.TickApiDbContext; + +#nullable disable + +namespace TickAPI.Migrations +{ + [DbContext(typeof(TickApiDbContext))] + [Migration("20250508114210_UsedTicket")] + partial class UsedTicket + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("CategoryEvent", b => + { + b.Property("CategoriesId") + .HasColumnType("uniqueidentifier"); + + b.Property("EventsId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("CategoriesId", "EventsId"); + + b.HasIndex("EventsId"); + + b.ToTable("CategoryEvent"); + }); + + modelBuilder.Entity("TickAPI.Addresses.Models.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("City") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Country") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FlatNumber") + .HasColumnType("bigint"); + + b.Property("HouseNumber") + .HasColumnType("bigint"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Street") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Addresses"); + }); + + modelBuilder.Entity("TickAPI.Admins.Models.Admin", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Login") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Admins"); + }); + + modelBuilder.Entity("TickAPI.Categories.Models.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + + b.HasData( + new + { + Id = new Guid("ec3daf69-baa9-4fcd-a674-c09884a57272"), + Name = "Music" + }, + new + { + Id = new Guid("de89dd76-3b29-43e1-8f4b-5278b1b8bde2"), + Name = "Sports" + }, + new + { + Id = new Guid("ea58370b-2a17-4770-abea-66399ad69fb8"), + Name = "Conferences" + }, + new + { + Id = new Guid("4a086d9e-59de-4fd1-a1b2-bd9b5eec797c"), + Name = "Theatre" + }, + new + { + Id = new Guid("5f8dbe65-30be-453f-8f22-191a11b2977b"), + Name = "Comedy" + }, + new + { + Id = new Guid("4421327a-4bc8-4706-bec0-666f78ed0c69"), + Name = "Workshops" + }); + }); + + modelBuilder.Entity("TickAPI.Customers.Models.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Event", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AddressId") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("EventStatus") + .HasColumnType("int"); + + b.Property("MinimumAge") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OrganizerId") + .HasColumnType("uniqueidentifier"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("AddressId"); + + b.HasIndex("OrganizerId"); + + b.ToTable("Events"); + }); + + modelBuilder.Entity("TickAPI.Organizers.Models.Organizer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsVerified") + .HasColumnType("bit"); + + b.Property("LastName") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Organizers"); + }); + + modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AvailableFrom") + .HasColumnType("datetime2"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EventId") + .HasColumnType("uniqueidentifier"); + + b.Property("MaxCount") + .HasColumnType("bigint"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("EventId"); + + b.ToTable("TicketTypes"); + }); + + modelBuilder.Entity("TickAPI.Tickets.Models.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ForResell") + .HasColumnType("bit"); + + b.Property("NameOnTicket") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("uniqueidentifier"); + + b.Property("Seats") + .HasColumnType("nvarchar(max)"); + + b.Property("TypeId") + .HasColumnType("uniqueidentifier"); + + b.Property("Used") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("TypeId"); + + b.ToTable("Tickets"); + }); + + modelBuilder.Entity("CategoryEvent", b => + { + b.HasOne("TickAPI.Categories.Models.Category", null) + .WithMany() + .HasForeignKey("CategoriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TickAPI.Events.Models.Event", null) + .WithMany() + .HasForeignKey("EventsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Event", b => + { + b.HasOne("TickAPI.Addresses.Models.Address", "Address") + .WithMany() + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TickAPI.Organizers.Models.Organizer", "Organizer") + .WithMany("Events") + .HasForeignKey("OrganizerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Address"); + + b.Navigation("Organizer"); + }); + + modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => + { + b.HasOne("TickAPI.Events.Models.Event", "Event") + .WithMany("TicketTypes") + .HasForeignKey("EventId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Event"); + }); + + modelBuilder.Entity("TickAPI.Tickets.Models.Ticket", b => + { + b.HasOne("TickAPI.Customers.Models.Customer", "Owner") + .WithMany("Tickets") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TickAPI.TicketTypes.Models.TicketType", "Type") + .WithMany("Tickets") + .HasForeignKey("TypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + + b.Navigation("Type"); + }); + + modelBuilder.Entity("TickAPI.Customers.Models.Customer", b => + { + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Event", b => + { + b.Navigation("TicketTypes"); + }); + + modelBuilder.Entity("TickAPI.Organizers.Models.Organizer", b => + { + b.Navigation("Events"); + }); + + modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => + { + b.Navigation("Tickets"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TickAPI/TickAPI/Migrations/20250508114210_UsedTicket.cs b/TickAPI/TickAPI/Migrations/20250508114210_UsedTicket.cs new file mode 100644 index 0000000..852c68f --- /dev/null +++ b/TickAPI/TickAPI/Migrations/20250508114210_UsedTicket.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TickAPI.Migrations +{ + /// + public partial class UsedTicket : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Used", + table: "Tickets", + type: "bit", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Used", + table: "Tickets"); + } + } +} diff --git a/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs b/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs index 8866990..50fd0cc 100644 --- a/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs +++ b/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs @@ -288,6 +288,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("TypeId") .HasColumnType("uniqueidentifier"); + b.Property("Used") + .HasColumnType("bit"); + b.HasKey("Id"); b.HasIndex("OwnerId"); diff --git a/TickAPI/TickAPI/Tickets/Abstractions/ITicketFilter.cs b/TickAPI/TickAPI/Tickets/Abstractions/ITicketFilter.cs new file mode 100644 index 0000000..56dc48e --- /dev/null +++ b/TickAPI/TickAPI/Tickets/Abstractions/ITicketFilter.cs @@ -0,0 +1,13 @@ +using TickAPI.Tickets.Models; + +namespace TickAPI.Tickets.Abstractions; + +public interface ITicketFilter +{ + public IQueryable GetTickets(); + public void FilterUsedTickets(); + public void FilterUnusedTickets(); + public void FilterTicketsForResell(); + public void FilterTicketsNotForResell(); + public void FilterTicketsByEventName(string name); +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Abstractions/ITicketFilterApplier.cs b/TickAPI/TickAPI/Tickets/Abstractions/ITicketFilterApplier.cs new file mode 100644 index 0000000..1c76a38 --- /dev/null +++ b/TickAPI/TickAPI/Tickets/Abstractions/ITicketFilterApplier.cs @@ -0,0 +1,9 @@ +using TickAPI.Tickets.DTOs.Request; +using TickAPI.Tickets.Models; + +namespace TickAPI.Tickets.Abstractions; + +public interface ITicketFilterApplier +{ + public IQueryable ApplyFilters(TicketFiltersDto filters); +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs b/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs index 37b6384..028353b 100644 --- a/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs +++ b/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs @@ -1,5 +1,6 @@ using TickAPI.Common.Pagination.Responses; using TickAPI.Common.Results.Generic; +using TickAPI.Tickets.DTOs.Request; using TickAPI.Tickets.DTOs.Response; using TickAPI.TicketTypes.Models; @@ -12,5 +13,5 @@ public interface ITicketService public Task>> GetTicketsForResellAsync(Guid eventId, int page, int pageSize); public Task>> GetTicketsForCustomerAsync(string email, int page, - int pageSize); + int pageSize, TicketFiltersDto ? ticketFilters = null); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs b/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs index 78979fb..c6838e6 100644 --- a/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs +++ b/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs @@ -5,6 +5,8 @@ using TickAPI.Tickets.Abstractions; using TickAPI.Tickets.DTOs.Response; using TickAPI.Common.Pagination.Responses; +using TickAPI.Events.DTOs.Request; +using TickAPI.Tickets.DTOs.Request; namespace TickAPI.Tickets.Controllers; @@ -51,14 +53,14 @@ public async Task>> Ge [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] [HttpGet] - public async Task>> GetTicketsForCustomer([FromQuery] int pageSize, [FromQuery] int page) + public async Task>> GetTicketsForCustomer([FromQuery] int pageSize, [FromQuery] int page, [FromQuery] TicketFiltersDto filters) { var emailResult = _claimsService.GetEmailFromClaims(User.Claims); if (emailResult.IsError) { return StatusCode(emailResult.StatusCode, emailResult.ErrorMsg); } - var tickets = await _ticketService.GetTicketsForCustomerAsync(emailResult.Value!, page, pageSize); + var tickets = await _ticketService.GetTicketsForCustomerAsync(emailResult.Value!, page, pageSize, filters); if (tickets.IsError) { return StatusCode(tickets.StatusCode, tickets.ErrorMsg); diff --git a/TickAPI/TickAPI/Tickets/DTOs/Request/TicketFiltersDto.cs b/TickAPI/TickAPI/Tickets/DTOs/Request/TicketFiltersDto.cs new file mode 100644 index 0000000..b6204f4 --- /dev/null +++ b/TickAPI/TickAPI/Tickets/DTOs/Request/TicketFiltersDto.cs @@ -0,0 +1,10 @@ +namespace TickAPI.Tickets.DTOs.Request; + +public record TicketFiltersDto +{ + public bool usedOnly { get; set; } + public bool unusedOnly { get; set; } + public bool forResellOnly { get; set; } + public bool notForResellOnly { get; set; } + public string ? EventName { get; set; } +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/DTOs/Response/GetTicketForCustomerDto.cs b/TickAPI/TickAPI/Tickets/DTOs/Response/GetTicketForCustomerDto.cs index ca94458..db50365 100644 --- a/TickAPI/TickAPI/Tickets/DTOs/Response/GetTicketForCustomerDto.cs +++ b/TickAPI/TickAPI/Tickets/DTOs/Response/GetTicketForCustomerDto.cs @@ -2,6 +2,7 @@ public record GetTicketForCustomerDto ( + Guid TicketId, string EventName, DateTime EventStartDate, DateTime EventEndDate diff --git a/TickAPI/TickAPI/Tickets/Filters/TicketFilter.cs b/TickAPI/TickAPI/Tickets/Filters/TicketFilter.cs new file mode 100644 index 0000000..3e08cd9 --- /dev/null +++ b/TickAPI/TickAPI/Tickets/Filters/TicketFilter.cs @@ -0,0 +1,44 @@ +using TickAPI.Tickets.Abstractions; +using TickAPI.Tickets.Models; + +namespace TickAPI.Tickets.Filters; + +public class TicketFilter : ITicketFilter +{ + IQueryable _tickets; + + public TicketFilter(IQueryable tickets) + { + _tickets = tickets; + } + + public IQueryable GetTickets() + { + return _tickets; + } + + public void FilterUsedTickets() + { + _tickets = _tickets.Where(t => t.Used); + } + + public void FilterUnusedTickets() + { + _tickets = _tickets.Where(t => !t.Used); + } + + public void FilterTicketsForResell() + { + _tickets = _tickets.Where(t => t.ForResell); + } + + public void FilterTicketsNotForResell() + { + _tickets = _tickets.Where(t => !t.ForResell); + } + + public void FilterTicketsByEventName(string name) + { + _tickets = _tickets.Where(t => t.Type.Event.Name.ToLower().Contains(name.ToLower())); + } +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Filters/TicketFilterApplier.cs b/TickAPI/TickAPI/Tickets/Filters/TicketFilterApplier.cs new file mode 100644 index 0000000..b7154b3 --- /dev/null +++ b/TickAPI/TickAPI/Tickets/Filters/TicketFilterApplier.cs @@ -0,0 +1,37 @@ +using TickAPI.Tickets.Abstractions; +using TickAPI.Tickets.DTOs.Request; +using TickAPI.Tickets.Models; + +namespace TickAPI.Tickets.Filters; + +public class TicketFilterApplier +{ + private readonly ITicketFilter _ticketFilter; + private readonly Dictionary, Action> _filterActions; + + public TicketFilterApplier(ITicketFilter ticketFilter) + { + _ticketFilter = ticketFilter; + _filterActions = new Dictionary, Action> + { + { f => !string.IsNullOrEmpty(f.EventName), f => _ticketFilter.FilterTicketsByEventName(f.EventName!) }, + { f => f.usedOnly, f => _ticketFilter.FilterUsedTickets() }, + { f => f.unusedOnly, f => _ticketFilter.FilterUnusedTickets() }, + { f => f.forResellOnly, f => _ticketFilter.FilterTicketsForResell() }, + { f => f.notForResellOnly, f => _ticketFilter.FilterTicketsNotForResell() }, + }; + } + + public IQueryable ApplyFilters(TicketFiltersDto filters) + { + foreach (var (condition, apply) in _filterActions) + { + if (condition(filters)) + { + apply(filters); + } + } + + return _ticketFilter.GetTickets(); + } +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Models/Ticket.cs b/TickAPI/TickAPI/Tickets/Models/Ticket.cs index 21f765f..49a68ac 100644 --- a/TickAPI/TickAPI/Tickets/Models/Ticket.cs +++ b/TickAPI/TickAPI/Tickets/Models/Ticket.cs @@ -11,4 +11,5 @@ public class Ticket public string NameOnTicket { get; set; } public string? Seats { get; set; } public bool ForResell { get; set; } + public bool Used { get; set; } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Services/TicketService.cs b/TickAPI/TickAPI/Tickets/Services/TicketService.cs index d003a6e..7b7fa2e 100644 --- a/TickAPI/TickAPI/Tickets/Services/TicketService.cs +++ b/TickAPI/TickAPI/Tickets/Services/TicketService.cs @@ -2,7 +2,9 @@ using TickAPI.Common.Pagination.Responses; using TickAPI.Common.Results.Generic; using TickAPI.Tickets.Abstractions; +using TickAPI.Tickets.DTOs.Request; using TickAPI.Tickets.DTOs.Response; +using TickAPI.Tickets.Filters; using TickAPI.TicketTypes.Models; namespace TickAPI.Tickets.Services; @@ -48,9 +50,15 @@ public async Task>> GetTicke return Result>.Success(paginatedResult); } //TODO: Maybe apply some filtering over here? - public async Task>> GetTicketsForCustomerAsync(string email, int page, int pageSize) + public async Task>> GetTicketsForCustomerAsync(string email, int page, int pageSize, TicketFiltersDto ? ticketFilters = null) { var customerTickets = _ticketRepository.GetTicketsByCustomerEmail(email); + if (ticketFilters != null) + { + var filter = new TicketFilter(customerTickets); + var applier = new TicketFilterApplier(filter); + customerTickets = applier.ApplyFilters(ticketFilters); + } var paginatedCustomerTickets = await _paginationService.PaginateAsync(customerTickets, pageSize, page); if (paginatedCustomerTickets.IsError) { @@ -58,7 +66,7 @@ public async Task>> GetTicketsForC } var paginatedResult = _paginationService.MapData(paginatedCustomerTickets.Value!, - t => new GetTicketForCustomerDto(t.Type.Event.Name, t.Type.Event.StartDate, t.Type.Event.EndDate)); + t => new GetTicketForCustomerDto(t.Id, t.Type.Event.Name, t.Type.Event.StartDate, t.Type.Event.EndDate)); return Result>.Success(paginatedResult); } From 715b5262004363f739b5f6189348b26c74da388f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Fri, 9 May 2025 07:31:20 +0200 Subject: [PATCH 30/80] tests --- .../Filters/TicketFilterApplierTests.cs | 183 ++++++++++++++++ .../Tickets/Filters/TicketFilterTests.cs | 201 ++++++++++++++++++ 2 files changed, 384 insertions(+) create mode 100644 TickAPI/TickAPI.Tests/Tickets/Filters/TicketFilterApplierTests.cs create mode 100644 TickAPI/TickAPI.Tests/Tickets/Filters/TicketFilterTests.cs diff --git a/TickAPI/TickAPI.Tests/Tickets/Filters/TicketFilterApplierTests.cs b/TickAPI/TickAPI.Tests/Tickets/Filters/TicketFilterApplierTests.cs new file mode 100644 index 0000000..0a81f6a --- /dev/null +++ b/TickAPI/TickAPI.Tests/Tickets/Filters/TicketFilterApplierTests.cs @@ -0,0 +1,183 @@ +using Moq; +using TickAPI.Tickets.Abstractions; +using TickAPI.Tickets.DTOs.Request; +using TickAPI.Tickets.Filters; +using TickAPI.Tickets.Models; + +namespace TickAPI.Tests.Tickets.Filters; + +public class TicketFilterApplierTests +{ + private readonly Mock _mockTicketFilter; + private readonly TicketFilterApplier _ticketFilterApplier; + private readonly IQueryable _emptyQueryable = new List().AsQueryable(); + + public TicketFilterApplierTests() + { + _mockTicketFilter = new Mock(); + _mockTicketFilter.Setup(tf => tf.GetTickets()).Returns(_emptyQueryable); + _ticketFilterApplier = new TicketFilterApplier(_mockTicketFilter.Object); + } + + [Fact] + public void ApplyFilters_WithEventName_ShouldCallFilterTicketsByEventName() + { + // Arrange + var filters = new TicketFiltersDto + { + EventName = "concert", + usedOnly = false, + unusedOnly = false, + forResellOnly = false, + notForResellOnly = false + }; + + // Act + _ticketFilterApplier.ApplyFilters(filters); + + // Assert + _mockTicketFilter.Verify(tf => tf.FilterTicketsByEventName(filters.EventName!), Times.Once); + _mockTicketFilter.Verify(tf => tf.GetTickets(), Times.Once); + } + + [Fact] + public void ApplyFilters_WithUsedOnly_ShouldCallFilterUsedTickets() + { + // Arrange + var filters = new TicketFiltersDto + { + EventName = null, + usedOnly = true, + unusedOnly = false, + forResellOnly = false, + notForResellOnly = false + }; + + // Act + _ticketFilterApplier.ApplyFilters(filters); + + // Assert + _mockTicketFilter.Verify(tf => tf.FilterUsedTickets(), Times.Once); + _mockTicketFilter.Verify(tf => tf.GetTickets(), Times.Once); + } + + [Fact] + public void ApplyFilters_WithUnusedOnly_ShouldCallFilterUnusedTickets() + { + // Arrange + var filters = new TicketFiltersDto + { + EventName = null, + usedOnly = false, + unusedOnly = true, + forResellOnly = false, + notForResellOnly = false + }; + + // Act + _ticketFilterApplier.ApplyFilters(filters); + + // Assert + _mockTicketFilter.Verify(tf => tf.FilterUnusedTickets(), Times.Once); + _mockTicketFilter.Verify(tf => tf.GetTickets(), Times.Once); + } + + [Fact] + public void ApplyFilters_WithForResellOnly_ShouldCallFilterTicketsForResell() + { + // Arrange + var filters = new TicketFiltersDto + { + EventName = null, + usedOnly = false, + unusedOnly = false, + forResellOnly = true, + notForResellOnly = false + }; + + // Act + _ticketFilterApplier.ApplyFilters(filters); + + // Assert + _mockTicketFilter.Verify(tf => tf.FilterTicketsForResell(), Times.Once); + _mockTicketFilter.Verify(tf => tf.GetTickets(), Times.Once); + } + + [Fact] + public void ApplyFilters_WithNotForResellOnly_ShouldCallFilterTicketsNotForResell() + { + // Arrange + var filters = new TicketFiltersDto + { + EventName = null, + usedOnly = false, + unusedOnly = false, + forResellOnly = false, + notForResellOnly = true + }; + + // Act + _ticketFilterApplier.ApplyFilters(filters); + + // Assert + _mockTicketFilter.Verify(tf => tf.FilterTicketsNotForResell(), Times.Once); + _mockTicketFilter.Verify(tf => tf.GetTickets(), Times.Once); + } + + [Fact] + public void ApplyFilters_WithMultipleFilters_ShouldCallAllRelevantFilters() + { + // Arrange + var filters = new TicketFiltersDto + { + EventName = "concert", + usedOnly = true, + unusedOnly = false, + forResellOnly = true, + notForResellOnly = false + }; + + // Act + _ticketFilterApplier.ApplyFilters(filters); + + // Assert + _mockTicketFilter.Verify(tf => tf.FilterTicketsByEventName(filters.EventName!), Times.Once); + _mockTicketFilter.Verify(tf => tf.FilterUsedTickets(), Times.Once); + _mockTicketFilter.Verify(tf => tf.FilterTicketsForResell(), Times.Once); + _mockTicketFilter.Verify(tf => tf.FilterUnusedTickets(), Times.Never); + _mockTicketFilter.Verify(tf => tf.FilterTicketsNotForResell(), Times.Never); + _mockTicketFilter.Verify(tf => tf.GetTickets(), Times.Once); + } + + [Fact] + public void ApplyFilters_WithNoFilters_ShouldOnlyCallGetTickets() + { + // Arrange + var expectedResult = new List + { + new Ticket { NameOnTicket = "Test Ticket" } + }.AsQueryable(); + _mockTicketFilter.Setup(tf => tf.GetTickets()).Returns(expectedResult); + var filters = new TicketFiltersDto + { + EventName = null, + usedOnly = false, + unusedOnly = false, + forResellOnly = false, + notForResellOnly = false + }; + + // Act + var result = _ticketFilterApplier.ApplyFilters(filters); + + // Assert + _mockTicketFilter.Verify(tf => tf.FilterTicketsByEventName(It.IsAny()), Times.Never); + _mockTicketFilter.Verify(tf => tf.FilterUsedTickets(), Times.Never); + _mockTicketFilter.Verify(tf => tf.FilterUnusedTickets(), Times.Never); + _mockTicketFilter.Verify(tf => tf.FilterTicketsForResell(), Times.Never); + _mockTicketFilter.Verify(tf => tf.FilterTicketsNotForResell(), Times.Never); + _mockTicketFilter.Verify(tf => tf.GetTickets(), Times.Once); + Assert.Same(expectedResult, result); + } + +} \ No newline at end of file diff --git a/TickAPI/TickAPI.Tests/Tickets/Filters/TicketFilterTests.cs b/TickAPI/TickAPI.Tests/Tickets/Filters/TicketFilterTests.cs new file mode 100644 index 0000000..10031b4 --- /dev/null +++ b/TickAPI/TickAPI.Tests/Tickets/Filters/TicketFilterTests.cs @@ -0,0 +1,201 @@ +using TickAPI.Customers.Models; +using TickAPI.Events.Models; +using TickAPI.Tickets.Filters; +using TickAPI.Tickets.Models; +using TickAPI.TicketTypes.Models; + +namespace TickAPI.Tests.Tickets.Filters; + +public class TicketFilterTests +{ + private static List GetTestTickets() => + [ + new Ticket + { + Id = Guid.NewGuid(), + Type = new TicketType + { + Price = 100, + Event = new Event + { + Name = "Concert A", + Description = "An amazing rock concert" + } + }, + Owner = new Customer { FirstName = "John", LastName = "Doe" }, + NameOnTicket = "John Doe", + Seats = "A12", + ForResell = true, + Used = false + }, + + new Ticket + { + Id = Guid.NewGuid(), + Type = new TicketType + { + Price = 50, + Event = new Event + { + Name = "Concert B", + Description = "Chill jazz night" + } + }, + Owner = new Customer { FirstName = "Jane", LastName = "Smith" }, + NameOnTicket = "Jane Smith", + Seats = "B5", + ForResell = false, + Used = true + }, + + new Ticket + { + Id = Guid.NewGuid(), + Type = new TicketType + { + Price = 200, + Event = new Event + { + Name = "Conference", + Description = "Tech event for developers" + } + }, + Owner = new Customer { FirstName = "Mike", LastName = "Johnson" }, + NameOnTicket = "Mike Johnson", + Seats = "C8", + ForResell = false, + Used = false + } + ]; + + [Fact] + public void FilterUsedTickets_ShouldReturnOnlyUsedTickets() + { + // Arrange + var tickets = GetTestTickets(); + var sut = new TicketFilter(tickets.AsQueryable()); + + //Act + sut.FilterUsedTickets(); + var result = sut.GetTickets().ToList(); + + // Assert + Assert.Single(result); + Assert.Contains(tickets[1], result); + } + + [Fact] + public void FilterUnusedTickets_ShouldReturnOnlyUnusedTickets() + { + // Arrange + var tickets = GetTestTickets(); + var sut = new TicketFilter(tickets.AsQueryable()); + + // Act + sut.FilterUnusedTickets(); + var result = sut.GetTickets().ToList(); + + // Assert + Assert.Equal(2, result.Count); + Assert.Contains(tickets[0], result); + Assert.Contains(tickets[2], result); + } + + [Fact] + public void FilterTicketsForResell_ShouldReturnOnlyTicketsForResell() + { + // Arrange + var tickets = GetTestTickets(); + var sut = new TicketFilter(tickets.AsQueryable()); + + //Act + sut.FilterTicketsForResell(); + var result = sut.GetTickets().ToList(); + + // Assert + Assert.Single(result); + Assert.Contains(tickets[0], result); + } + + [Fact] + public void FilterTicketsNotForResell_ShouldReturnOnlyTicketsNotForResell() + { + // Arrange + var tickets = GetTestTickets(); + var sut = new TicketFilter(tickets.AsQueryable()); + + // Act + sut.FilterTicketsNotForResell(); + var result = sut.GetTickets().ToList(); + + // Assert + Assert.Equal(2, result.Count); + Assert.Contains(tickets[1], result); + Assert.Contains(tickets[2], result); + } + + [Fact] + public void FilterTicketsByEventName_ShouldReturnTicketsWithMatchingEventName() + { + // Arrange + var tickets = GetTestTickets(); + + // Act + var ticketFilter = new TicketFilter(tickets.AsQueryable()); + ticketFilter.FilterTicketsByEventName("concert"); + var result = ticketFilter.GetTickets().ToList(); + + // Assert + Assert.Equal(2, result.Count); + Assert.Contains(tickets[0], result); + Assert.Contains(tickets[1], result); + } + + [Fact] + public void FilterTicketsByEventName_CaseInsensitive_ShouldReturnMatchingTickets() + { + // Arrange + var tickets = GetTestTickets(); + + // Act + var ticketFilter = new TicketFilter(tickets.AsQueryable()); + ticketFilter.FilterTicketsByEventName("cONcErt a"); + var result = ticketFilter.GetTickets().ToList(); + + // Assert + Assert.Single(result); + Assert.Contains(tickets[0], result); + } + + [Fact] + public void FilterTicketsByEventName_WithNoMatches_ShouldReturnEmptyList() + { + // Arrange + var tickets = GetTestTickets(); + + // Act + var ticketFilter = new TicketFilter(tickets.AsQueryable()); + ticketFilter.FilterTicketsByEventName("nonexistent event"); + var result = ticketFilter.GetTickets().ToList(); + + // Assert + Assert.Empty(result); + } + + [Fact] + public void GetTickets_WithNoFilters_ShouldReturnAllTickets() + { + // Arrange + var tickets = GetTestTickets(); + + // Act + var ticketFilter = new TicketFilter(tickets.AsQueryable()); + var result = ticketFilter.GetTickets().ToList(); + + // Assert + Assert.Equal(3, result.Count); + Assert.Contains(tickets[0], result); + Assert.Contains(tickets[1], result); + Assert.Contains(tickets[2], result); + } +} \ No newline at end of file From 4758983a862993d5546ded20d0d12352a6da04a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Fri, 9 May 2025 15:24:01 +0200 Subject: [PATCH 31/80] Add qr service and scannin endpoint --- .../Common/QR/Services/QRCodeService.cs | 22 +++++++++++++++++++ TickAPI/TickAPI/TickAPI.csproj | 2 ++ .../Tickets/Abstractions/ITicketRepository.cs | 1 + .../Tickets/Abstractions/ITicketService.cs | 1 + .../Tickets/Controllers/TicketsController.cs | 11 ++++++++++ TickAPI/TickAPI/Tickets/Models/Ticket.cs | 1 + .../Tickets/Repositories/TicketRepository.cs | 15 +++++++++++++ .../TickAPI/Tickets/Services/TicketService.cs | 11 ++++++++++ 8 files changed, 64 insertions(+) create mode 100644 TickAPI/TickAPI/Common/QR/Services/QRCodeService.cs diff --git a/TickAPI/TickAPI/Common/QR/Services/QRCodeService.cs b/TickAPI/TickAPI/Common/QR/Services/QRCodeService.cs new file mode 100644 index 0000000..2ead564 --- /dev/null +++ b/TickAPI/TickAPI/Common/QR/Services/QRCodeService.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNetCore.Components.RenderTree; +using QRCoder; +using System; +using System.Collections.Generic; +using System.Drawing.Imaging; +using System.IO; +using Microsoft.AspNetCore.Mvc; +using QRCoder; + +namespace TickAPI.Common.QR.Services; + +public class QRCodeService +{ + public byte[] GenerateQrCode(Guid ticketId) + { + var qrGenerator = new QRCodeGenerator(); + var qrData = qrGenerator.CreateQrCode(ticketId.ToString(), QRCodeGenerator.ECCLevel.Q); + var qrCode = new PngByteQRCode(qrData); + var qrCodeImage = qrCode.GetGraphic(20); + return qrCodeImage; + } +} \ No newline at end of file diff --git a/TickAPI/TickAPI/TickAPI.csproj b/TickAPI/TickAPI/TickAPI.csproj index a4ca3a8..fb79365 100644 --- a/TickAPI/TickAPI/TickAPI.csproj +++ b/TickAPI/TickAPI/TickAPI.csproj @@ -18,6 +18,8 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs b/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs index a09c57d..8b27c3c 100644 --- a/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs +++ b/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs @@ -10,4 +10,5 @@ public interface ITicketRepository public Task> GetTicketWithDetailsByIdAndEmailAsync(Guid id, string email); public IQueryable GetTicketsByEventId(Guid eventId); public IQueryable GetTicketsByCustomerEmail(string email); + public Task> MarkTicketAsUsed(Guid id); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs b/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs index 37b6384..6f2c924 100644 --- a/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs +++ b/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs @@ -13,4 +13,5 @@ public Task>> GetTicketsForR int pageSize); public Task>> GetTicketsForCustomerAsync(string email, int page, int pageSize); + public Task> ScanTicket(Guid ticketGuid); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs b/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs index 78979fb..eec9221 100644 --- a/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs +++ b/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs @@ -65,4 +65,15 @@ public async Task>> GetTicke } return Ok(tickets.Value); } + + [HttpPost("/scan/{id:guid}")] + public async Task> ScanTicket(Guid id) + { + var res = await _ticketService.ScanTicket(id); + if (res.IsError) + { + return StatusCode(res.StatusCode, res.ErrorMsg); + } + return Ok(res.Value); + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Models/Ticket.cs b/TickAPI/TickAPI/Tickets/Models/Ticket.cs index 21f765f..5106f9f 100644 --- a/TickAPI/TickAPI/Tickets/Models/Ticket.cs +++ b/TickAPI/TickAPI/Tickets/Models/Ticket.cs @@ -11,4 +11,5 @@ public class Ticket public string NameOnTicket { get; set; } public string? Seats { get; set; } public bool ForResell { get; set; } + public bool Used {get; set;} } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs b/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs index 2ec02c2..c2cb18c 100644 --- a/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs +++ b/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs @@ -49,4 +49,19 @@ public async Task> GetTicketWithDetailsByIdAndEmailAsync(Guid id, } return Result.Success(ticket); } + + public async Task> MarkTicketAsUsed(Guid id) + { + var ticket = await _tickApiDbContext.Tickets.FirstOrDefaultAsync(t => t.Id == id); + + if (ticket != null) + { + ticket.Used = true; + await _tickApiDbContext.SaveChangesAsync(); // 🔄 This actually writes changes to the DB + return Result.Success(true); + } + + return Result.Failure(StatusCodes.Status404NotFound, "Ticket with this id doesn't exist"); + + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Services/TicketService.cs b/TickAPI/TickAPI/Tickets/Services/TicketService.cs index d003a6e..abf3ec1 100644 --- a/TickAPI/TickAPI/Tickets/Services/TicketService.cs +++ b/TickAPI/TickAPI/Tickets/Services/TicketService.cs @@ -89,4 +89,15 @@ public async Task> GetTicketDetailsAsync(Gui ); return Result.Success(ticketDetails); } + + public async Task> ScanTicket(Guid ticketGuid) + { + var res = await _ticketRepository.MarkTicketAsUsed(ticketGuid); + if (res.IsError) + { + return Result.PropagateError(res); + } + + return res; + } } \ No newline at end of file From 943de02b0965e66dade8842174864fcdc6aac7d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Fri, 9 May 2025 15:37:06 +0200 Subject: [PATCH 32/80] Add url encoding instead of guid and add qrcode to ticketdetails --- .../Common/QR/Abstractions/IQRCodeService.cs | 6 ++++++ .../TickAPI/Common/QR/Services/QRCodeService.cs | 15 +++++---------- TickAPI/TickAPI/Program.cs | 3 +++ .../DTOs/Response/GetTicketDetailsResponseDto.cs | 3 ++- TickAPI/TickAPI/Tickets/Services/TicketService.cs | 12 +++++++++--- 5 files changed, 25 insertions(+), 14 deletions(-) create mode 100644 TickAPI/TickAPI/Common/QR/Abstractions/IQRCodeService.cs diff --git a/TickAPI/TickAPI/Common/QR/Abstractions/IQRCodeService.cs b/TickAPI/TickAPI/Common/QR/Abstractions/IQRCodeService.cs new file mode 100644 index 0000000..5e04e95 --- /dev/null +++ b/TickAPI/TickAPI/Common/QR/Abstractions/IQRCodeService.cs @@ -0,0 +1,6 @@ +namespace TickAPI.Common.QR.Abstractions; + +public interface IQRCodeService +{ + public byte[] GenerateQrCode(Guid ticketId); +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/QR/Services/QRCodeService.cs b/TickAPI/TickAPI/Common/QR/Services/QRCodeService.cs index 2ead564..f763cdf 100644 --- a/TickAPI/TickAPI/Common/QR/Services/QRCodeService.cs +++ b/TickAPI/TickAPI/Common/QR/Services/QRCodeService.cs @@ -1,20 +1,15 @@ -using Microsoft.AspNetCore.Components.RenderTree; -using QRCoder; -using System; -using System.Collections.Generic; -using System.Drawing.Imaging; -using System.IO; -using Microsoft.AspNetCore.Mvc; -using QRCoder; +using QRCoder; +using TickAPI.Common.QR.Abstractions; namespace TickAPI.Common.QR.Services; -public class QRCodeService +public class QRCodeService : IQRCodeService { public byte[] GenerateQrCode(Guid ticketId) { var qrGenerator = new QRCodeGenerator(); - var qrData = qrGenerator.CreateQrCode(ticketId.ToString(), QRCodeGenerator.ECCLevel.Q); + var url = "localhost:5124/scan/" + ticketId; + var qrData = qrGenerator.CreateQrCode(url, QRCodeGenerator.ECCLevel.Q); var qrCode = new PngByteQRCode(qrData); var qrCodeImage = qrCode.GetGraphic(20); return qrCodeImage; diff --git a/TickAPI/TickAPI/Program.cs b/TickAPI/TickAPI/Program.cs index 0a431a1..8a0ac0e 100644 --- a/TickAPI/TickAPI/Program.cs +++ b/TickAPI/TickAPI/Program.cs @@ -42,6 +42,8 @@ using TickAPI.Common.Payment.Abstractions; using TickAPI.Common.Payment.Health; using TickAPI.Common.Payment.Services; +using TickAPI.Common.QR.Abstractions; +using TickAPI.Common.QR.Services; // Builder constants const string allowClientPolicyName = "AllowClient"; @@ -126,6 +128,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); diff --git a/TickAPI/TickAPI/Tickets/DTOs/Response/GetTicketDetailsResponseDto.cs b/TickAPI/TickAPI/Tickets/DTOs/Response/GetTicketDetailsResponseDto.cs index 46032d5..17f4fc5 100644 --- a/TickAPI/TickAPI/Tickets/DTOs/Response/GetTicketDetailsResponseDto.cs +++ b/TickAPI/TickAPI/Tickets/DTOs/Response/GetTicketDetailsResponseDto.cs @@ -13,5 +13,6 @@ public record GetTicketDetailsResponseDto DateTime StartDate, DateTime EndDate, GetTicketDetailsAddressDto Address, - Guid eventId + Guid eventId, + string qrcode ); \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Services/TicketService.cs b/TickAPI/TickAPI/Tickets/Services/TicketService.cs index abf3ec1..5a59451 100644 --- a/TickAPI/TickAPI/Tickets/Services/TicketService.cs +++ b/TickAPI/TickAPI/Tickets/Services/TicketService.cs @@ -1,5 +1,6 @@ using TickAPI.Common.Pagination.Abstractions; using TickAPI.Common.Pagination.Responses; +using TickAPI.Common.QR.Abstractions; using TickAPI.Common.Results.Generic; using TickAPI.Tickets.Abstractions; using TickAPI.Tickets.DTOs.Response; @@ -11,11 +12,12 @@ public class TicketService : ITicketService { private readonly ITicketRepository _ticketRepository; private readonly IPaginationService _paginationService; - - public TicketService(ITicketRepository ticketRepository, IPaginationService paginationService) + private readonly IQRCodeService _qrCodeService; + public TicketService(ITicketRepository ticketRepository, IPaginationService paginationService, IQRCodeService qrCodeService) { _ticketRepository = ticketRepository; _paginationService = paginationService; + _qrCodeService = qrCodeService; } // TODO: Update this method to also count tickets cached in Redis as unavailable @@ -74,6 +76,9 @@ public async Task> GetTicketDetailsAsync(Gui var ev = ticket.Type.Event; var address = new GetTicketDetailsAddressDto(ev.Address.Country, ev.Address.City, ev.Address.PostalCode, ev.Address.Street, ev.Address.HouseNumber, ev.Address.FlatNumber); + + var qrbytes = _qrCodeService.GenerateQrCode(ticketGuid); + var qrcode = Convert.ToBase64String(qrbytes); var ticketDetails = new GetTicketDetailsResponseDto ( ticket.NameOnTicket, @@ -85,7 +90,8 @@ public async Task> GetTicketDetailsAsync(Gui ticket.Type.Event.StartDate, ticket.Type.Event.EndDate, address, - ticket.Type.Event.Id + ticket.Type.Event.Id, + qrcode ); return Result.Success(ticketDetails); } From b209d463c573b2895fb8a832e7c6b1df48976752 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Fri, 9 May 2025 23:54:34 +0200 Subject: [PATCH 33/80] removed unnecessary using directive --- TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs index a02c8ee..8c89917 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs @@ -1,5 +1,4 @@ using TickAPI.Common.Results; -using TickAPI.Common.Results.Generic; using TickAPI.ShoppingCarts.Abstractions; using TickAPI.Tickets.Models; From 1a862db8f2be5a02758fc0a9f76c64b61d7651d0 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Sat, 10 May 2025 12:10:47 +0200 Subject: [PATCH 34/80] Replace `Name` and `Description` with `SearchQuery` --- .../Controllers/EventsControllerTests.cs | 2 +- .../Events/Filters/EventFilterApplierTests.cs | 102 +++++------------- .../Events/DTOs/Request/EventFiltersDto.cs | 3 +- .../Events/Filters/EventFilterApplier.cs | 8 +- 4 files changed, 33 insertions(+), 82 deletions(-) diff --git a/TickAPI/TickAPI.Tests/Events/Controllers/EventsControllerTests.cs b/TickAPI/TickAPI.Tests/Events/Controllers/EventsControllerTests.cs index 69b4f4e..cd0f793 100644 --- a/TickAPI/TickAPI.Tests/Events/Controllers/EventsControllerTests.cs +++ b/TickAPI/TickAPI.Tests/Events/Controllers/EventsControllerTests.cs @@ -20,7 +20,7 @@ namespace TickAPI.Tests.Events.Controllers; public class EventsControllerTests { - private readonly EventFiltersDto _emptyFilters = new EventFiltersDto(null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null); + private readonly EventFiltersDto _emptyFilters = new EventFiltersDto(null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null); [Fact] public async Task CreateEvent_WhenDataIsValid_ShouldReturnSuccess() diff --git a/TickAPI/TickAPI.Tests/Events/Filters/EventFilterApplierTests.cs b/TickAPI/TickAPI.Tests/Events/Filters/EventFilterApplierTests.cs index b836508..6256303 100644 --- a/TickAPI/TickAPI.Tests/Events/Filters/EventFilterApplierTests.cs +++ b/TickAPI/TickAPI.Tests/Events/Filters/EventFilterApplierTests.cs @@ -24,8 +24,7 @@ public void ApplyFilters_WithName_ShouldCallFilterByName() { // Arrange var filters = new EventFiltersDto( - Name: "test event", - Descritpion: null, + SearchQuery: "test event", StartDate: null, MinStartDate: null, MaxStartDate: null, @@ -49,52 +48,18 @@ public void ApplyFilters_WithName_ShouldCallFilterByName() _eventFilterApplier.ApplyFilters(filters); // Assert - _mockEventFilter.Verify(ef => ef.FilterByName(filters.Name!), Times.Once); + _mockEventFilter.Verify(ef => ef.FilterByName(filters.SearchQuery!), Times.Once); + _mockEventFilter.Verify(ef => ef.FilterByDescription(filters.SearchQuery!), Times.Once); _mockEventFilter.Verify(ef => ef.GetEvents(), Times.Once); } - - [Fact] - public void ApplyFilters_WithDescription_ShouldCallFilterByDescription() - { - // Arrange - var filters = new EventFiltersDto( - Name: null, - Descritpion: "test description", - StartDate: null, - MinStartDate: null, - MaxStartDate: null, - EndDate: null, - MinEndDate: null, - MaxEndDate: null, - MinPrice: null, - MaxPrice: null, - MinAge: null, - MaxMinimumAge: null, - AddressCountry: null, - AddressCity: null, - AddressStreet: null, - HouseNumber: null, - FlatNumber: null, - PostalCode: null, - CategoriesNames: null - ); - - // Act - _eventFilterApplier.ApplyFilters(filters); - - // Assert - _mockEventFilter.Verify(ef => ef.FilterByDescription(filters.Descritpion!), Times.Once); - _mockEventFilter.Verify(ef => ef.GetEvents(), Times.Once); - } - + [Fact] public void ApplyFilters_WithStartDate_ShouldCallFilterByStartDate() { // Arrange var startDate = new DateTime(2025, 5, 1); var filters = new EventFiltersDto( - Name: null, - Descritpion: null, + SearchQuery: null, StartDate: startDate, MinStartDate: null, MaxStartDate: null, @@ -128,8 +93,7 @@ public void ApplyFilters_WithMinStartDate_ShouldCallFilterByMinStartDate() // Arrange var minStartDate = new DateTime(2025, 5, 1); var filters = new EventFiltersDto( - Name: null, - Descritpion: null, + SearchQuery: null, StartDate: null, MinStartDate: minStartDate, MaxStartDate: null, @@ -163,8 +127,7 @@ public void ApplyFilters_WithMaxStartDate_ShouldCallFilterByMaxStartDate() // Arrange var maxStartDate = new DateTime(2025, 5, 1); var filters = new EventFiltersDto( - Name: null, - Descritpion: null, + SearchQuery: null, StartDate: null, MinStartDate: null, MaxStartDate: maxStartDate, @@ -198,8 +161,7 @@ public void ApplyFilters_WithEndDate_ShouldCallFilterByEndDate() // Arrange var endDate = new DateTime(2025, 5, 1); var filters = new EventFiltersDto( - Name: null, - Descritpion: null, + SearchQuery: null, StartDate: null, MinStartDate: null, MaxStartDate: null, @@ -233,8 +195,7 @@ public void ApplyFilters_WithMinEndDate_ShouldCallFilterByMinEndDate() // Arrange var minEndDate = new DateTime(2025, 5, 1); var filters = new EventFiltersDto( - Name: null, - Descritpion: null, + SearchQuery: null, StartDate: null, MinStartDate: null, MaxStartDate: null, @@ -268,8 +229,7 @@ public void ApplyFilters_WithMaxEndDate_ShouldCallFilterByMaxEndDate() // Arrange var maxEndDate = new DateTime(2025, 5, 1); var filters = new EventFiltersDto( - Name: null, - Descritpion: null, + SearchQuery: null, StartDate: null, MinStartDate: null, MaxStartDate: null, @@ -303,8 +263,7 @@ public void ApplyFilters_WithMinPrice_ShouldCallFilterByMinPrice() // Arrange decimal minPrice = 100; var filters = new EventFiltersDto( - Name: null, - Descritpion: null, + SearchQuery: null, StartDate: null, MinStartDate: null, MaxStartDate: null, @@ -338,8 +297,7 @@ public void ApplyFilters_WithMaxPrice_ShouldCallFilterByMaxPrice() // Arrange decimal maxPrice = 200; var filters = new EventFiltersDto( - Name: null, - Descritpion: null, + SearchQuery: null, StartDate: null, MinStartDate: null, MaxStartDate: null, @@ -373,8 +331,7 @@ public void ApplyFilters_WithMinAge_ShouldCallFilterByMinAge() // Arrange uint minAge = 18; var filters = new EventFiltersDto( - Name: null, - Descritpion: null, + SearchQuery: null, StartDate: null, MinStartDate: null, MaxStartDate: null, @@ -408,8 +365,7 @@ public void ApplyFilters_WithMaxMinimumAge_ShouldCallFilterByMaxMinimumAge() // Arrange uint maxMinimumAge = 21; var filters = new EventFiltersDto( - Name: null, - Descritpion: null, + SearchQuery: null, StartDate: null, MinStartDate: null, MaxStartDate: null, @@ -442,8 +398,7 @@ public void ApplyFilters_WithAddressCountry_ShouldCallFilterByAddressCountry() { // Arrange var filters = new EventFiltersDto( - Name: null, - Descritpion: null, + SearchQuery: null, StartDate: null, MinStartDate: null, MaxStartDate: null, @@ -476,8 +431,7 @@ public void ApplyFilters_WithAddressCity_ShouldCallFilterByAddressCity() { // Arrange var filters = new EventFiltersDto( - Name: null, - Descritpion: null, + SearchQuery: null, StartDate: null, MinStartDate: null, MaxStartDate: null, @@ -510,8 +464,7 @@ public void ApplyFilters_WithAddressStreetOnly_ShouldCallFilterByAddressStreet() { // Arrange var filters = new EventFiltersDto( - Name: null, - Descritpion: null, + SearchQuery: null, StartDate: null, MinStartDate: null, MaxStartDate: null, @@ -545,8 +498,7 @@ public void ApplyFilters_WithAddressStreetAndHouseNumber_ShouldCallFilterByAddre // Arrange uint houseNumber = 12; var filters = new EventFiltersDto( - Name: null, - Descritpion: null, + SearchQuery: null, StartDate: null, MinStartDate: null, MaxStartDate: null, @@ -581,8 +533,7 @@ public void ApplyFilters_WithCompleteAddress_ShouldCallFilterByAddressStreet() uint houseNumber = 12; uint flatNumber = 5; var filters = new EventFiltersDto( - Name: null, - Descritpion: null, + SearchQuery: null, StartDate: null, MinStartDate: null, MaxStartDate: null, @@ -615,8 +566,7 @@ public void ApplyFilters_WithPostalCode_ShouldCallFilterByAddressPostalCode() { // Arrange var filters = new EventFiltersDto( - Name: null, - Descritpion: null, + SearchQuery: null, StartDate: null, MinStartDate: null, MaxStartDate: null, @@ -650,8 +600,7 @@ public void ApplyFilters_WithCategoriesNames_ShouldCallFilterByCategoriesNames() // Arrange var categoriesNames = new List { "Concert", "Festival", "Exhibition" }; var filters = new EventFiltersDto( - Name: null, - Descritpion: null, + SearchQuery: null, StartDate: null, MinStartDate: null, MaxStartDate: null, @@ -687,8 +636,7 @@ public void ApplyFilters_WithMultipleFilters_ShouldCallAllRelevantFilters() decimal minPrice = 50; decimal maxPrice = 200; var filters = new EventFiltersDto( - Name: "Concert", - Descritpion: null, + SearchQuery: "Concert", StartDate: startDate, MinStartDate: null, MaxStartDate: null, @@ -712,7 +660,8 @@ public void ApplyFilters_WithMultipleFilters_ShouldCallAllRelevantFilters() _eventFilterApplier.ApplyFilters(filters); // Assert - _mockEventFilter.Verify(ef => ef.FilterByName(filters.Name!), Times.Once); + _mockEventFilter.Verify(ef => ef.FilterByName(filters.SearchQuery!), Times.Once); + _mockEventFilter.Verify(ef => ef.FilterByDescription(filters.SearchQuery!), Times.Once); _mockEventFilter.Verify(ef => ef.FilterByStartDate(filters.StartDate!.Value), Times.Once); _mockEventFilter.Verify(ef => ef.FilterByMinPrice(filters.MinPrice!.Value), Times.Once); _mockEventFilter.Verify(ef => ef.FilterByMaxPrice(filters.MaxPrice!.Value), Times.Once); @@ -730,8 +679,7 @@ public void ApplyFilters_WithNoFilters_ShouldOnlyCallGetEvents() }.AsQueryable(); _mockEventFilter.Setup(ef => ef.GetEvents()).Returns(expectedResult); var filters = new EventFiltersDto( - Name: null, - Descritpion: null, + SearchQuery: null, StartDate: null, MinStartDate: null, MaxStartDate: null, diff --git a/TickAPI/TickAPI/Events/DTOs/Request/EventFiltersDto.cs b/TickAPI/TickAPI/Events/DTOs/Request/EventFiltersDto.cs index 7eba343..89ece02 100644 --- a/TickAPI/TickAPI/Events/DTOs/Request/EventFiltersDto.cs +++ b/TickAPI/TickAPI/Events/DTOs/Request/EventFiltersDto.cs @@ -1,8 +1,7 @@ namespace TickAPI.Events.DTOs.Request; public record EventFiltersDto( - string? Name, - string? Descritpion, + string? SearchQuery, DateTime? StartDate, DateTime? MinStartDate, DateTime? MaxStartDate, diff --git a/TickAPI/TickAPI/Events/Filters/EventFilterApplier.cs b/TickAPI/TickAPI/Events/Filters/EventFilterApplier.cs index a9f890b..7a3aa80 100644 --- a/TickAPI/TickAPI/Events/Filters/EventFilterApplier.cs +++ b/TickAPI/TickAPI/Events/Filters/EventFilterApplier.cs @@ -14,8 +14,12 @@ public EventFilterApplier(IEventFilter eventFilter) _eventFilter = eventFilter; _filterActions = new Dictionary, Action> { - { f => !string.IsNullOrEmpty(f.Name), f => _eventFilter.FilterByName(f.Name!) }, - { f => !string.IsNullOrEmpty(f.Descritpion), f => _eventFilter.FilterByDescription(f.Descritpion!) }, + { f => !string.IsNullOrEmpty(f.SearchQuery), f => + { + _eventFilter.FilterByName(f.SearchQuery!); + _eventFilter.FilterByDescription(f.SearchQuery!); + } + }, { f => f.StartDate.HasValue, f => _eventFilter.FilterByStartDate(f.StartDate!.Value) }, { f => f.MinStartDate.HasValue, f => _eventFilter.FilterByMinStartDate(f.MinStartDate!.Value) }, { f => f.MaxStartDate.HasValue, f => _eventFilter.FilterByMaxStartDate(f.MaxStartDate!.Value) }, From 0bbfe5ba60cb97ef8c64d8e787d9998a9e8f9f07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Sat, 10 May 2025 12:19:56 +0200 Subject: [PATCH 35/80] trzcinskik comments --- .../Filters/TicketFilterApplierTests.cs | 92 ++++++++----------- .../Tickets/Services/TicketServiceTests.cs | 6 +- .../Tickets/DTOs/Request/TicketFiltersDto.cs | 24 +++-- .../DTOs/Response/GetTicketForCustomerDto.cs | 3 +- .../Tickets/Filters/TicketFilterApplier.cs | 8 +- .../TickAPI/Tickets/Services/TicketService.cs | 3 +- 6 files changed, 67 insertions(+), 69 deletions(-) diff --git a/TickAPI/TickAPI.Tests/Tickets/Filters/TicketFilterApplierTests.cs b/TickAPI/TickAPI.Tests/Tickets/Filters/TicketFilterApplierTests.cs index 0a81f6a..1bf1ecf 100644 --- a/TickAPI/TickAPI.Tests/Tickets/Filters/TicketFilterApplierTests.cs +++ b/TickAPI/TickAPI.Tests/Tickets/Filters/TicketFilterApplierTests.cs @@ -24,13 +24,11 @@ public void ApplyFilters_WithEventName_ShouldCallFilterTicketsByEventName() { // Arrange var filters = new TicketFiltersDto - { - EventName = "concert", - usedOnly = false, - unusedOnly = false, - forResellOnly = false, - notForResellOnly = false - }; + ( + null, + null, + "concert" + ); // Act _ticketFilterApplier.ApplyFilters(filters); @@ -45,13 +43,11 @@ public void ApplyFilters_WithUsedOnly_ShouldCallFilterUsedTickets() { // Arrange var filters = new TicketFiltersDto - { - EventName = null, - usedOnly = true, - unusedOnly = false, - forResellOnly = false, - notForResellOnly = false - }; + ( + UsageFilter.OnlyUsed, + null, + null + ); // Act _ticketFilterApplier.ApplyFilters(filters); @@ -66,13 +62,11 @@ public void ApplyFilters_WithUnusedOnly_ShouldCallFilterUnusedTickets() { // Arrange var filters = new TicketFiltersDto - { - EventName = null, - usedOnly = false, - unusedOnly = true, - forResellOnly = false, - notForResellOnly = false - }; + ( + UsageFilter.OnlyNotUsed, + null, + null + ); // Act _ticketFilterApplier.ApplyFilters(filters); @@ -87,13 +81,11 @@ public void ApplyFilters_WithForResellOnly_ShouldCallFilterTicketsForResell() { // Arrange var filters = new TicketFiltersDto - { - EventName = null, - usedOnly = false, - unusedOnly = false, - forResellOnly = true, - notForResellOnly = false - }; + ( + null, + ResellFilter.OnlyForResell, + null + ); // Act _ticketFilterApplier.ApplyFilters(filters); @@ -108,13 +100,11 @@ public void ApplyFilters_WithNotForResellOnly_ShouldCallFilterTicketsNotForResel { // Arrange var filters = new TicketFiltersDto - { - EventName = null, - usedOnly = false, - unusedOnly = false, - forResellOnly = false, - notForResellOnly = true - }; + ( + null, + ResellFilter.OnlyNotForResell, + null + ); // Act _ticketFilterApplier.ApplyFilters(filters); @@ -129,23 +119,21 @@ public void ApplyFilters_WithMultipleFilters_ShouldCallAllRelevantFilters() { // Arrange var filters = new TicketFiltersDto - { - EventName = "concert", - usedOnly = true, - unusedOnly = false, - forResellOnly = true, - notForResellOnly = false - }; + ( + UsageFilter.OnlyNotUsed, + ResellFilter.OnlyNotForResell, + "concert" + ); // Act _ticketFilterApplier.ApplyFilters(filters); // Assert _mockTicketFilter.Verify(tf => tf.FilterTicketsByEventName(filters.EventName!), Times.Once); - _mockTicketFilter.Verify(tf => tf.FilterUsedTickets(), Times.Once); - _mockTicketFilter.Verify(tf => tf.FilterTicketsForResell(), Times.Once); - _mockTicketFilter.Verify(tf => tf.FilterUnusedTickets(), Times.Never); - _mockTicketFilter.Verify(tf => tf.FilterTicketsNotForResell(), Times.Never); + _mockTicketFilter.Verify(tf => tf.FilterUsedTickets(), Times.Never); + _mockTicketFilter.Verify(tf => tf.FilterTicketsForResell(), Times.Never); + _mockTicketFilter.Verify(tf => tf.FilterUnusedTickets(), Times.Once); + _mockTicketFilter.Verify(tf => tf.FilterTicketsNotForResell(), Times.Once); _mockTicketFilter.Verify(tf => tf.GetTickets(), Times.Once); } @@ -159,13 +147,11 @@ public void ApplyFilters_WithNoFilters_ShouldOnlyCallGetTickets() }.AsQueryable(); _mockTicketFilter.Setup(tf => tf.GetTickets()).Returns(expectedResult); var filters = new TicketFiltersDto - { - EventName = null, - usedOnly = false, - unusedOnly = false, - forResellOnly = false, - notForResellOnly = false - }; + ( + null, + null, + null + ); // Act var result = _ticketFilterApplier.ApplyFilters(filters); diff --git a/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs b/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs index 6ff2147..389f7a4 100644 --- a/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs @@ -448,6 +448,7 @@ public async Task GetTicketsForCustomerAsync_WithValidInput_ReturnsSuccessResult new Ticket { Id = new Guid(), + Used = false, Type = new TicketType { Event = new Event @@ -461,6 +462,7 @@ public async Task GetTicketsForCustomerAsync_WithValidInput_ReturnsSuccessResult new Ticket { Id = new Guid(), + Used = false, Type = new TicketType { Event = new Event @@ -482,8 +484,8 @@ public async Task GetTicketsForCustomerAsync_WithValidInput_ReturnsSuccessResult false, new PaginationDetails(0, 2) ); - var mappedData1 = new GetTicketForCustomerDto(tickets[0].Id, "EventName", new DateTime(2025, 10, 10), new DateTime(2025, 10, 20)); - var mappedData2 = new GetTicketForCustomerDto(tickets[1].Id, "EventName2", new DateTime(2025, 11, 10), new DateTime(2025, 11, 20)); + var mappedData1 = new GetTicketForCustomerDto(tickets[0].Id, "EventName", new DateTime(2025, 10, 10), new DateTime(2025, 10, 20), false); + var mappedData2 = new GetTicketForCustomerDto(tickets[1].Id, "EventName2", new DateTime(2025, 11, 10), new DateTime(2025, 11, 20), false); var mappedPaginatedData = new PaginatedData ( new List{mappedData1, mappedData2}, diff --git a/TickAPI/TickAPI/Tickets/DTOs/Request/TicketFiltersDto.cs b/TickAPI/TickAPI/Tickets/DTOs/Request/TicketFiltersDto.cs index b6204f4..19cbf47 100644 --- a/TickAPI/TickAPI/Tickets/DTOs/Request/TicketFiltersDto.cs +++ b/TickAPI/TickAPI/Tickets/DTOs/Request/TicketFiltersDto.cs @@ -1,10 +1,20 @@ namespace TickAPI.Tickets.DTOs.Request; -public record TicketFiltersDto +public enum UsageFilter +{ + OnlyUsed, + OnlyNotUsed +} + +public enum ResellFilter { - public bool usedOnly { get; set; } - public bool unusedOnly { get; set; } - public bool forResellOnly { get; set; } - public bool notForResellOnly { get; set; } - public string ? EventName { get; set; } -} \ No newline at end of file + OnlyForResell, + OnlyNotForResell +} + +public record TicketFiltersDto +( + UsageFilter? Usage, + ResellFilter? Resell, + string? EventName +); \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/DTOs/Response/GetTicketForCustomerDto.cs b/TickAPI/TickAPI/Tickets/DTOs/Response/GetTicketForCustomerDto.cs index db50365..4584d76 100644 --- a/TickAPI/TickAPI/Tickets/DTOs/Response/GetTicketForCustomerDto.cs +++ b/TickAPI/TickAPI/Tickets/DTOs/Response/GetTicketForCustomerDto.cs @@ -5,5 +5,6 @@ public record GetTicketForCustomerDto Guid TicketId, string EventName, DateTime EventStartDate, - DateTime EventEndDate + DateTime EventEndDate, + bool Used ); \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Filters/TicketFilterApplier.cs b/TickAPI/TickAPI/Tickets/Filters/TicketFilterApplier.cs index b7154b3..2b9bf6e 100644 --- a/TickAPI/TickAPI/Tickets/Filters/TicketFilterApplier.cs +++ b/TickAPI/TickAPI/Tickets/Filters/TicketFilterApplier.cs @@ -15,10 +15,10 @@ public TicketFilterApplier(ITicketFilter ticketFilter) _filterActions = new Dictionary, Action> { { f => !string.IsNullOrEmpty(f.EventName), f => _ticketFilter.FilterTicketsByEventName(f.EventName!) }, - { f => f.usedOnly, f => _ticketFilter.FilterUsedTickets() }, - { f => f.unusedOnly, f => _ticketFilter.FilterUnusedTickets() }, - { f => f.forResellOnly, f => _ticketFilter.FilterTicketsForResell() }, - { f => f.notForResellOnly, f => _ticketFilter.FilterTicketsNotForResell() }, + { f => f.Usage == UsageFilter.OnlyUsed, f => _ticketFilter.FilterUsedTickets() }, + { f => f.Usage == UsageFilter.OnlyNotUsed, f => _ticketFilter.FilterUnusedTickets() }, + { f => f.Resell == ResellFilter.OnlyForResell, f => _ticketFilter.FilterTicketsForResell() }, + { f => f.Resell == ResellFilter.OnlyNotForResell, f => _ticketFilter.FilterTicketsNotForResell() }, }; } diff --git a/TickAPI/TickAPI/Tickets/Services/TicketService.cs b/TickAPI/TickAPI/Tickets/Services/TicketService.cs index 7b7fa2e..36c7c4c 100644 --- a/TickAPI/TickAPI/Tickets/Services/TicketService.cs +++ b/TickAPI/TickAPI/Tickets/Services/TicketService.cs @@ -49,7 +49,6 @@ public async Task>> GetTicke t => new GetTicketForResellResponseDto(t.Id, t.Type.Price, t.Type.Currency, t.Type.Description, t.Seats)); return Result>.Success(paginatedResult); } - //TODO: Maybe apply some filtering over here? public async Task>> GetTicketsForCustomerAsync(string email, int page, int pageSize, TicketFiltersDto ? ticketFilters = null) { var customerTickets = _ticketRepository.GetTicketsByCustomerEmail(email); @@ -66,7 +65,7 @@ public async Task>> GetTicketsForC } var paginatedResult = _paginationService.MapData(paginatedCustomerTickets.Value!, - t => new GetTicketForCustomerDto(t.Id, t.Type.Event.Name, t.Type.Event.StartDate, t.Type.Event.EndDate)); + t => new GetTicketForCustomerDto(t.Id, t.Type.Event.Name, t.Type.Event.StartDate, t.Type.Event.EndDate, t.Used)); return Result>.Success(paginatedResult); } From 12d579cd1e5875ff1d4fa1e6a301dd844cd22846 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Sat, 10 May 2025 17:13:20 +0200 Subject: [PATCH 36/80] Add retrieving urls --- .../TickAPI/Common/QR/Abstractions/IQRCodeService.cs | 2 +- TickAPI/TickAPI/Common/QR/Services/QRCodeService.cs | 3 +-- .../TickAPI/Tickets/Abstractions/ITicketService.cs | 4 +++- .../TickAPI/Tickets/Controllers/TicketsController.cs | 10 ++++++---- TickAPI/TickAPI/Tickets/Services/TicketService.cs | 11 +++++++---- 5 files changed, 18 insertions(+), 12 deletions(-) diff --git a/TickAPI/TickAPI/Common/QR/Abstractions/IQRCodeService.cs b/TickAPI/TickAPI/Common/QR/Abstractions/IQRCodeService.cs index 5e04e95..94b2690 100644 --- a/TickAPI/TickAPI/Common/QR/Abstractions/IQRCodeService.cs +++ b/TickAPI/TickAPI/Common/QR/Abstractions/IQRCodeService.cs @@ -2,5 +2,5 @@ public interface IQRCodeService { - public byte[] GenerateQrCode(Guid ticketId); + public byte[] GenerateQrCode(string url); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/QR/Services/QRCodeService.cs b/TickAPI/TickAPI/Common/QR/Services/QRCodeService.cs index f763cdf..1986f90 100644 --- a/TickAPI/TickAPI/Common/QR/Services/QRCodeService.cs +++ b/TickAPI/TickAPI/Common/QR/Services/QRCodeService.cs @@ -5,10 +5,9 @@ namespace TickAPI.Common.QR.Services; public class QRCodeService : IQRCodeService { - public byte[] GenerateQrCode(Guid ticketId) + public byte[] GenerateQrCode(string url) { var qrGenerator = new QRCodeGenerator(); - var url = "localhost:5124/scan/" + ticketId; var qrData = qrGenerator.CreateQrCode(url, QRCodeGenerator.ECCLevel.Q); var qrCode = new PngByteQRCode(qrData); var qrCodeImage = qrCode.GetGraphic(20); diff --git a/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs b/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs index 76b1b06..abca918 100644 --- a/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs +++ b/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs @@ -9,10 +9,12 @@ namespace TickAPI.Tickets.Abstractions; public interface ITicketService { public Result GetNumberOfAvailableTicketsByType(TicketType ticketType); - public Task> GetTicketDetailsAsync(Guid ticketGuid, string email); public Task>> GetTicketsForResellAsync(Guid eventId, int page, int pageSize); public Task>> GetTicketsForCustomerAsync(string email, int page, int pageSize, TicketFiltersDto ? ticketFilters = null); public Task> ScanTicket(Guid ticketGuid); + + public Task> GetTicketDetailsAsync(Guid ticketGuid, string email, + string scanUrl); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs b/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs index 969a974..2473b96 100644 --- a/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs +++ b/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs @@ -31,11 +31,12 @@ public async Task> GetTicketDetails(Gu return emailResult.ToObjectResult(); } var email = emailResult.Value!; - var ticket = await _ticketService.GetTicketDetailsAsync(id, email); + string? scanTicketUrl = Url.Action("ScanTicket", "Tickets", new { id = id }, Request.Scheme); + var ticket = await _ticketService.GetTicketDetailsAsync(id, email, scanTicketUrl); return ticket.ToObjectResult(); } - [HttpGet("/for-resell")] + [HttpGet("for-resell")] public async Task>> GetTicketsForResell([FromQuery] Guid eventId, [FromQuery] int pageSize, [FromQuery] int page) { var result = await _ticketService.GetTicketsForResellAsync(eventId, page, pageSize); @@ -55,8 +56,8 @@ public async Task>> GetTicke return tickets.ToObjectResult(); } - [HttpPost("/scan/{id:guid}")] - public async Task> ScanTicket(Guid id) + [HttpGet("scan/{id:guid}")] + public async Task> ScanTicket([FromQuery] Guid id) { var res = await _ticketService.ScanTicket(id); if (res.IsError) @@ -65,4 +66,5 @@ public async Task> ScanTicket(Guid id) } return Ok(res.Value); } + } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Services/TicketService.cs b/TickAPI/TickAPI/Tickets/Services/TicketService.cs index e6987c8..0da33d2 100644 --- a/TickAPI/TickAPI/Tickets/Services/TicketService.cs +++ b/TickAPI/TickAPI/Tickets/Services/TicketService.cs @@ -1,4 +1,5 @@ -using TickAPI.Common.Pagination.Abstractions; +using Azure.Core; +using TickAPI.Common.Pagination.Abstractions; using TickAPI.Common.Pagination.Responses; using TickAPI.Common.QR.Abstractions; using TickAPI.Common.Results.Generic; @@ -72,7 +73,7 @@ public async Task>> GetTicketsForC return Result>.Success(paginatedResult); } - public async Task> GetTicketDetailsAsync(Guid ticketGuid, string email) + public async Task> GetTicketDetailsAsync(Guid ticketGuid, string email, string scanUrl) { var ticketRes = await _ticketRepository.GetTicketWithDetailsByIdAndEmailAsync(ticketGuid, email); if (ticketRes.IsError) @@ -83,8 +84,8 @@ public async Task> GetTicketDetailsAsync(Gui var ev = ticket.Type.Event; var address = new GetTicketDetailsAddressDto(ev.Address.Country, ev.Address.City, ev.Address.PostalCode, ev.Address.Street, ev.Address.HouseNumber, ev.Address.FlatNumber); - - var qrbytes = _qrCodeService.GenerateQrCode(ticketGuid); + + var qrbytes = _qrCodeService.GenerateQrCode(scanUrl); var qrcode = Convert.ToBase64String(qrbytes); var ticketDetails = new GetTicketDetailsResponseDto ( @@ -113,4 +114,6 @@ public async Task> ScanTicket(Guid ticketGuid) return res; } + + } \ No newline at end of file From 1cda709ccaa38fcfcc0de7d62f3e06d2a53a2b6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Sat, 10 May 2025 17:21:14 +0200 Subject: [PATCH 37/80] Handle scanning used tickets --- .../Tickets/Controllers/TicketsController.cs | 3 ++- .../Tickets/Repositories/TicketRepository.cs | 13 +++++-------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs b/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs index 2473b96..f9f5512 100644 --- a/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs +++ b/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs @@ -5,6 +5,7 @@ using TickAPI.Tickets.Abstractions; using TickAPI.Tickets.DTOs.Response; using TickAPI.Common.Pagination.Responses; +using TickAPI.Common.Results; using TickAPI.Tickets.DTOs.Request; namespace TickAPI.Tickets.Controllers; @@ -32,7 +33,7 @@ public async Task> GetTicketDetails(Gu } var email = emailResult.Value!; string? scanTicketUrl = Url.Action("ScanTicket", "Tickets", new { id = id }, Request.Scheme); - var ticket = await _ticketService.GetTicketDetailsAsync(id, email, scanTicketUrl); + var ticket = await _ticketService.GetTicketDetailsAsync(id, email, scanTicketUrl!); return ticket.ToObjectResult(); } diff --git a/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs b/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs index c2cb18c..1180158 100644 --- a/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs +++ b/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs @@ -53,15 +53,12 @@ public async Task> GetTicketWithDetailsByIdAndEmailAsync(Guid id, public async Task> MarkTicketAsUsed(Guid id) { var ticket = await _tickApiDbContext.Tickets.FirstOrDefaultAsync(t => t.Id == id); - - if (ticket != null) + if (ticket == null || ticket.Used) { - ticket.Used = true; - await _tickApiDbContext.SaveChangesAsync(); // 🔄 This actually writes changes to the DB - return Result.Success(true); + return Result.Failure(StatusCodes.Status404NotFound, "Ticket with this id doesn't exist"); } - - return Result.Failure(StatusCodes.Status404NotFound, "Ticket with this id doesn't exist"); - + ticket.Used = true; + await _tickApiDbContext.SaveChangesAsync(); + return Result.Success(true); } } \ No newline at end of file From 102ff0689ec8685ae71de3661a65a3ff1a9065eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Sat, 10 May 2025 17:39:23 +0200 Subject: [PATCH 38/80] Add test --- .../Tickets/Services/TicketServiceTests.cs | 64 +++++++++++++++---- 1 file changed, 51 insertions(+), 13 deletions(-) diff --git a/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs b/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs index 389f7a4..8b46ba2 100644 --- a/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs @@ -4,6 +4,8 @@ using TickAPI.Addresses.Models; using TickAPI.Common.Pagination.Abstractions; using TickAPI.Common.Pagination.Responses; +using TickAPI.Common.QR.Abstractions; +using TickAPI.Common.Results; using TickAPI.Common.Results.Generic; using TickAPI.Customers.Models; using TickAPI.Events.Models; @@ -27,12 +29,13 @@ public void GetNumberOfAvailableTicketsByType_AmountsAreCorrect_ShouldReturnCorr var ticketRepositoryMock = new Mock(); var paginationServiceMock = new Mock(); + var qrServiceMock = new Mock(); ticketRepositoryMock .Setup(m => m.GetAllTicketsByTicketType(type)) .Returns(ticketList.AsQueryable()); - var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act var result = sut.GetNumberOfAvailableTicketsByType(type); @@ -51,12 +54,13 @@ public void GetNumberOfAvailableTicketsByType_WhenMoreTicketExistThanMaxCount_Sh var ticketRepositoryMock = new Mock(); var paginationServiceMock = new Mock(); + var qrServiceMock = new Mock(); ticketRepositoryMock .Setup(m => m.GetAllTicketsByTicketType(type)) .Returns(ticketList.AsQueryable()); - var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act var result = sut.GetNumberOfAvailableTicketsByType(type); @@ -150,7 +154,9 @@ public async Task GetTicketsForResellAsync_WhenDataIsValid_ShouldReturnSuccess() It.IsAny>())) .Returns(mappedData); - var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object); + var qrServiceMock = new Mock(); + + var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act var result = await sut.GetTicketsForResellAsync(eventId, page, pageSize); @@ -226,8 +232,9 @@ public async Task GetTicketsForResellAsync_WhenNoTicketsForResell_ShouldReturnEm paginatedData, It.IsAny>())) .Returns(mappedData); + var qrServiceMock = new Mock(); - var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act var result = await sut.GetTicketsForResellAsync(eventId, page, pageSize); @@ -270,7 +277,9 @@ public async Task GetTicketsForResellAsync_WhenPaginationFails_ShouldPropagateEr paginationServiceMock.Setup(p => p.PaginateAsync(It.IsAny>(), pageSize, page)) .ReturnsAsync(Result>.Failure(statusCode, errorMsg)); - var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object); + var qrServiceMock = new Mock(); + + var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act var result = await sut.GetTicketsForResellAsync(eventId, page, pageSize); @@ -321,8 +330,9 @@ public async Task GetTicketsForResellAsync_WhenNoTicketsForEvent_ShouldReturnEmp paginatedData, It.IsAny>())) .Returns(mappedData); + var qrServiceMock = new Mock(); - var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act var result = await sut.GetTicketsForResellAsync(eventId, page, pageSize); @@ -371,7 +381,7 @@ public async Task GetTicketDetailsAsync_WhenTicketExistsForTheUser_ShouldReturnT }, }; string email = "123@123.com"; - + string scanurl = "http://localhost"; Mock ticketRepositoryMock = new Mock(); var paginationServiceMock = new Mock(); @@ -379,11 +389,14 @@ public async Task GetTicketDetailsAsync_WhenTicketExistsForTheUser_ShouldReturnT ticketRepositoryMock.Setup(m => m.GetTicketWithDetailsByIdAndEmailAsync(ticket.Id, email)) .ReturnsAsync(Result.Success(ticket)); - var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object); + var qrServiceMock = new Mock(); + qrServiceMock.Setup(m => m.GenerateQrCode(scanurl)).Returns([]); + + var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act - var res = await sut.GetTicketDetailsAsync(ticket.Id, email); + var res = await sut.GetTicketDetailsAsync(ticket.Id, email, scanurl); // Assert @@ -415,18 +428,20 @@ public async Task GetTicketDetailsAsync_WhenTicketDoesNotExistForTheUser_ShouldR Guid ticketId = Guid.NewGuid(); string email = "123@123.com"; + string scanUrl = "http://localhost"; Mock ticketRepositoryMock = new Mock(); ticketRepositoryMock.Setup(m => m.GetTicketWithDetailsByIdAndEmailAsync(ticketId, email)). ReturnsAsync(Result.Failure(StatusCodes.Status404NotFound, "Ticket with this id doesn't exist " + "for this user")); var paginationServiceMock = new Mock(); + var qrServiceMock = new Mock(); - var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act - var res = await sut.GetTicketDetailsAsync(ticketId, email); + var res = await sut.GetTicketDetailsAsync(ticketId, email, scanUrl); // Assert @@ -505,7 +520,10 @@ public async Task GetTicketsForCustomerAsync_WithValidInput_ReturnsSuccessResult paginationServiceMock.Setup(p => p.MapData(paginatedData, It.IsAny>())) .Returns(mappedPaginatedData); - var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object); + + var qrServiceMock = new Mock(); + + var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act var result = await sut.GetTicketsForCustomerAsync(email, page, pageSize); @@ -543,7 +561,9 @@ public async Task GetTicketsForCustomerAsync_WhenUserHasNoTickets_ReturnsEmptyPa paginationServiceMock.Setup(p => p.MapData(emptyPaginatedData, It.IsAny>())) .Returns(mappedEmptyPaginatedData); - var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object); + var qrServiceMock = new Mock(); + + var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act var result = await sut.GetTicketsForCustomerAsync(email, page, pageSize); @@ -552,4 +572,22 @@ public async Task GetTicketsForCustomerAsync_WhenUserHasNoTickets_ReturnsEmptyPa Assert.True(result.IsSuccess); Assert.Empty(result.Value!.Data); } + + [Fact] + public async Task ScanTicket_WhenScanningSuccesful_ShouldReturnSuccess() + { + // Arrange + var guid = Guid.NewGuid(); + var ticketRepositoryMock = new Mock(); + ticketRepositoryMock.Setup(m => m.MarkTicketAsUsed(guid)).ReturnsAsync(Result.Success(true)); + var paginationServiceMock = new Mock(); + var qrServiceMock = new Mock(); + var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + + // Act + var res = await sut.ScanTicket(guid); + + // Assert + Assert.True(res.IsSuccess); + } } \ No newline at end of file From 5f6caeeb7ec11f512d2a4983d4c875b015d60127 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Sat, 10 May 2025 17:42:02 +0200 Subject: [PATCH 39/80] new spacing --- TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs | 2 +- TickAPI/TickAPI/Common/QR/Services/QRCodeService.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs b/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs index 8b46ba2..5a56bc2 100644 --- a/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs @@ -582,7 +582,7 @@ public async Task ScanTicket_WhenScanningSuccesful_ShouldReturnSuccess() ticketRepositoryMock.Setup(m => m.MarkTicketAsUsed(guid)).ReturnsAsync(Result.Success(true)); var paginationServiceMock = new Mock(); var qrServiceMock = new Mock(); - var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act var res = await sut.ScanTicket(guid); diff --git a/TickAPI/TickAPI/Common/QR/Services/QRCodeService.cs b/TickAPI/TickAPI/Common/QR/Services/QRCodeService.cs index 1986f90..9d5f5c1 100644 --- a/TickAPI/TickAPI/Common/QR/Services/QRCodeService.cs +++ b/TickAPI/TickAPI/Common/QR/Services/QRCodeService.cs @@ -10,7 +10,7 @@ public byte[] GenerateQrCode(string url) var qrGenerator = new QRCodeGenerator(); var qrData = qrGenerator.CreateQrCode(url, QRCodeGenerator.ECCLevel.Q); var qrCode = new PngByteQRCode(qrData); - var qrCodeImage = qrCode.GetGraphic(20); + var qrCodeImage = qrCode.GetGraphic(20); return qrCodeImage; } } \ No newline at end of file From c6a9a3f69dd1f7c880d5b32f00cbbc7b909afa33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Sun, 11 May 2025 12:30:01 +0200 Subject: [PATCH 40/80] Trzcinskik comments --- .../Tickets/Services/TicketServiceTests.cs | 2 +- .../TickAPI/Tickets/Abstractions/ITicketRepository.cs | 5 +++-- .../TickAPI/Tickets/Abstractions/ITicketService.cs | 4 ++-- .../TickAPI/Tickets/Controllers/TicketsController.cs | 8 ++------ .../TickAPI/Tickets/Repositories/TicketRepository.cs | 11 ++++++++--- TickAPI/TickAPI/Tickets/Services/TicketService.cs | 8 ++------ 6 files changed, 18 insertions(+), 20 deletions(-) diff --git a/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs b/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs index 5a56bc2..331853f 100644 --- a/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs @@ -579,7 +579,7 @@ public async Task ScanTicket_WhenScanningSuccesful_ShouldReturnSuccess() // Arrange var guid = Guid.NewGuid(); var ticketRepositoryMock = new Mock(); - ticketRepositoryMock.Setup(m => m.MarkTicketAsUsed(guid)).ReturnsAsync(Result.Success(true)); + ticketRepositoryMock.Setup(m => m.MarkTicketAsUsed(guid)).ReturnsAsync(Result.Success()); var paginationServiceMock = new Mock(); var qrServiceMock = new Mock(); var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); diff --git a/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs b/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs index 8b27c3c..6290950 100644 --- a/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs +++ b/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs @@ -1,4 +1,5 @@ -using TickAPI.Common.Results.Generic; +using TickAPI.Common.Results; +using TickAPI.Common.Results.Generic; using TickAPI.Tickets.Models; using TickAPI.TicketTypes.Models; @@ -10,5 +11,5 @@ public interface ITicketRepository public Task> GetTicketWithDetailsByIdAndEmailAsync(Guid id, string email); public IQueryable GetTicketsByEventId(Guid eventId); public IQueryable GetTicketsByCustomerEmail(string email); - public Task> MarkTicketAsUsed(Guid id); + public Task MarkTicketAsUsed(Guid id); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs b/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs index abca918..c722a8c 100644 --- a/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs +++ b/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs @@ -1,4 +1,5 @@ using TickAPI.Common.Pagination.Responses; +using TickAPI.Common.Results; using TickAPI.Common.Results.Generic; using TickAPI.Tickets.DTOs.Request; using TickAPI.Tickets.DTOs.Response; @@ -13,8 +14,7 @@ public Task>> GetTicketsForR int pageSize); public Task>> GetTicketsForCustomerAsync(string email, int page, int pageSize, TicketFiltersDto ? ticketFilters = null); - public Task> ScanTicket(Guid ticketGuid); - + public Task ScanTicket(Guid ticketGuid); public Task> GetTicketDetailsAsync(Guid ticketGuid, string email, string scanUrl); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs b/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs index f9f5512..77b45e2 100644 --- a/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs +++ b/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs @@ -58,14 +58,10 @@ public async Task>> GetTicke } [HttpGet("scan/{id:guid}")] - public async Task> ScanTicket([FromQuery] Guid id) + public async Task> ScanTicket(Guid id) { var res = await _ticketService.ScanTicket(id); - if (res.IsError) - { - return StatusCode(res.StatusCode, res.ErrorMsg); - } - return Ok(res.Value); + return res.ToObjectResult(); } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs b/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs index 1180158..fe4c5f1 100644 --- a/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs +++ b/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore; +using TickAPI.Common.Results; using TickAPI.Common.Results.Generic; using TickAPI.Common.TickApiDbContext; using TickAPI.Tickets.Abstractions; @@ -50,15 +51,19 @@ public async Task> GetTicketWithDetailsByIdAndEmailAsync(Guid id, return Result.Success(ticket); } - public async Task> MarkTicketAsUsed(Guid id) + public async Task MarkTicketAsUsed(Guid id) { var ticket = await _tickApiDbContext.Tickets.FirstOrDefaultAsync(t => t.Id == id); if (ticket == null || ticket.Used) { - return Result.Failure(StatusCodes.Status404NotFound, "Ticket with this id doesn't exist"); + return Result.Failure(StatusCodes.Status404NotFound, "Ticket with this id doesn't exist"); + } + if (ticket.Used) + { + return Result.Failure(StatusCodes.Status400BadRequest, "Ticket already used"); } ticket.Used = true; await _tickApiDbContext.SaveChangesAsync(); - return Result.Success(true); + return Result.Success(); } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Services/TicketService.cs b/TickAPI/TickAPI/Tickets/Services/TicketService.cs index 0da33d2..27b60fd 100644 --- a/TickAPI/TickAPI/Tickets/Services/TicketService.cs +++ b/TickAPI/TickAPI/Tickets/Services/TicketService.cs @@ -2,6 +2,7 @@ using TickAPI.Common.Pagination.Abstractions; using TickAPI.Common.Pagination.Responses; using TickAPI.Common.QR.Abstractions; +using TickAPI.Common.Results; using TickAPI.Common.Results.Generic; using TickAPI.Tickets.Abstractions; using TickAPI.Tickets.DTOs.Request; @@ -104,14 +105,9 @@ public async Task> GetTicketDetailsAsync(Gui return Result.Success(ticketDetails); } - public async Task> ScanTicket(Guid ticketGuid) + public async Task ScanTicket(Guid ticketGuid) { var res = await _ticketRepository.MarkTicketAsUsed(ticketGuid); - if (res.IsError) - { - return Result.PropagateError(res); - } - return res; } From af70c9e9e8d18afdeb39950534b7014a48e7d613 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Sun, 11 May 2025 13:45:41 +0200 Subject: [PATCH 41/80] if chang --- TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs b/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs index fe4c5f1..e5abb5e 100644 --- a/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs +++ b/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs @@ -54,7 +54,7 @@ public async Task> GetTicketWithDetailsByIdAndEmailAsync(Guid id, public async Task MarkTicketAsUsed(Guid id) { var ticket = await _tickApiDbContext.Tickets.FirstOrDefaultAsync(t => t.Id == id); - if (ticket == null || ticket.Used) + if (ticket == null) { return Result.Failure(StatusCodes.Status404NotFound, "Ticket with this id doesn't exist"); } From 64f3f062375c313e27a40dc1bd00d253c22b4dcc Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Sun, 11 May 2025 14:49:44 +0200 Subject: [PATCH 42/80] Add endpoint for editing events --- .../DTOs/Request/CreateAddressDto.cs | 8 +- .../DTOs/Request/EditEventCategoryDto.cs | 5 ++ .../Events/Abstractions/IEventRepository.cs | 5 +- .../Events/Abstractions/IEventService.cs | 1 + .../Events/Controllers/EventsController.cs | 27 +++++++ .../Events/DTOs/Request/EditEventDto.cs | 16 ++++ .../DTOs/Response/EditEventResponseDto.cs | 3 + .../Events/Repositories/EventRepository.cs | 21 +++++ .../TickAPI/Events/Services/EventService.cs | 76 ++++++++++++++++--- 9 files changed, 146 insertions(+), 16 deletions(-) create mode 100644 TickAPI/TickAPI/Categories/DTOs/Request/EditEventCategoryDto.cs create mode 100644 TickAPI/TickAPI/Events/DTOs/Request/EditEventDto.cs create mode 100644 TickAPI/TickAPI/Events/DTOs/Response/EditEventResponseDto.cs diff --git a/TickAPI/TickAPI/Addresses/DTOs/Request/CreateAddressDto.cs b/TickAPI/TickAPI/Addresses/DTOs/Request/CreateAddressDto.cs index 8aa9f4c..c79aecd 100644 --- a/TickAPI/TickAPI/Addresses/DTOs/Request/CreateAddressDto.cs +++ b/TickAPI/TickAPI/Addresses/DTOs/Request/CreateAddressDto.cs @@ -1,12 +1,10 @@ -using TickAPI.Events.Models; - -namespace TickAPI.Addresses.DTOs.Request; +namespace TickAPI.Addresses.DTOs.Request; public record CreateAddressDto( - string Country, string City, string? Street, uint? HouseNumber, uint? FlatNumber, - string PostalCode); \ No newline at end of file + string PostalCode +); \ No newline at end of file diff --git a/TickAPI/TickAPI/Categories/DTOs/Request/EditEventCategoryDto.cs b/TickAPI/TickAPI/Categories/DTOs/Request/EditEventCategoryDto.cs new file mode 100644 index 0000000..ae2d123 --- /dev/null +++ b/TickAPI/TickAPI/Categories/DTOs/Request/EditEventCategoryDto.cs @@ -0,0 +1,5 @@ +namespace TickAPI.Categories.DTOs.Request; + +public record EditEventCategoryDto( + string CategoryName +); diff --git a/TickAPI/TickAPI/Events/Abstractions/IEventRepository.cs b/TickAPI/TickAPI/Events/Abstractions/IEventRepository.cs index 8f4c560..a7e4cb1 100644 --- a/TickAPI/TickAPI/Events/Abstractions/IEventRepository.cs +++ b/TickAPI/TickAPI/Events/Abstractions/IEventRepository.cs @@ -1,4 +1,5 @@ -using TickAPI.Common.Results.Generic; +using TickAPI.Common.Results; +using TickAPI.Common.Results.Generic; using TickAPI.Events.Models; using TickAPI.Organizers.Models; @@ -10,4 +11,6 @@ public interface IEventRepository public IQueryable GetEvents(); public IQueryable GetEventsByOranizer(Organizer organizer); public Task> GetEventByIdAsync(Guid eventId); + public Task SaveEventAsync(Event ev); + public Task> GetEventByIdAndOrganizerAsync(Guid eventId, Organizer organizer); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/Abstractions/IEventService.cs b/TickAPI/TickAPI/Events/Abstractions/IEventService.cs index 3b4f020..b029b39 100644 --- a/TickAPI/TickAPI/Events/Abstractions/IEventService.cs +++ b/TickAPI/TickAPI/Events/Abstractions/IEventService.cs @@ -20,4 +20,5 @@ public Task> CreateNewEventAsync(string name, string description, public Task>> GetEventsAsync(int page, int pageSize, EventFiltersDto? eventFilters = null); public Task> GetEventsPaginationDetailsAsync(int pageSize); public Task> GetEventDetailsAsync(Guid eventId); + public Task> EditEventAsync(Organizer organizer, Guid eventId, string name, string description, DateTime startDate, DateTime endDate, uint? minimumAge, CreateAddressDto editAddress, List categories, EventStatus eventStatus); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/Controllers/EventsController.cs b/TickAPI/TickAPI/Events/Controllers/EventsController.cs index c1ff0e1..95cbf68 100644 --- a/TickAPI/TickAPI/Events/Controllers/EventsController.cs +++ b/TickAPI/TickAPI/Events/Controllers/EventsController.cs @@ -114,4 +114,31 @@ public async Task> GetEventDetails([Fro var eventDetailsResult = await _eventService.GetEventDetailsAsync(id); return eventDetailsResult.ToObjectResult(); } + + [AuthorizeWithPolicy(AuthPolicies.VerifiedOrganizerPolicy)] + [HttpPatch("{id:guid}")] + public async Task> EditEvent([FromRoute] Guid id, [FromBody] EditEventDto request) + { + var emailResult = _claimsService.GetEmailFromClaims(User.Claims); + if (emailResult.IsError) + { + return emailResult.ToObjectResult(); + } + var email = emailResult.Value!; + + var organizerResult = await _organizerService.GetOrganizerByEmailAsync(email); + if (organizerResult.IsError) + { + return organizerResult.ToObjectResult(); + } + var organizer = organizerResult.Value!; + + var editedEventResult = await _eventService.EditEventAsync(organizer, id, request.Name, request.Description, request.StartDate, request.EndDate, request.MinimumAge, + request.EditAddress, request.Categories, request.EventStatus); + + if (editedEventResult.IsError) + return editedEventResult.ToObjectResult(); + + return Ok("Event edited succesfully"); + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/DTOs/Request/EditEventDto.cs b/TickAPI/TickAPI/Events/DTOs/Request/EditEventDto.cs new file mode 100644 index 0000000..e441955 --- /dev/null +++ b/TickAPI/TickAPI/Events/DTOs/Request/EditEventDto.cs @@ -0,0 +1,16 @@ +using TickAPI.Addresses.DTOs.Request; +using TickAPI.Categories.DTOs.Request; +using TickAPI.Events.Models; + +namespace TickAPI.Events.DTOs.Request; + +public record EditEventDto( + string Name, + string Description, + DateTime StartDate, + DateTime EndDate, + uint? MinimumAge, + List Categories, + EventStatus EventStatus, + CreateAddressDto EditAddress +); diff --git a/TickAPI/TickAPI/Events/DTOs/Response/EditEventResponseDto.cs b/TickAPI/TickAPI/Events/DTOs/Response/EditEventResponseDto.cs new file mode 100644 index 0000000..455ecdc --- /dev/null +++ b/TickAPI/TickAPI/Events/DTOs/Response/EditEventResponseDto.cs @@ -0,0 +1,3 @@ +namespace TickAPI.Events.DTOs.Response; + +public record EditEventResponseDto(); diff --git a/TickAPI/TickAPI/Events/Repositories/EventRepository.cs b/TickAPI/TickAPI/Events/Repositories/EventRepository.cs index bdc59ee..bfd667e 100644 --- a/TickAPI/TickAPI/Events/Repositories/EventRepository.cs +++ b/TickAPI/TickAPI/Events/Repositories/EventRepository.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore; +using TickAPI.Common.Results; using TickAPI.Common.Results.Generic; using TickAPI.Common.TickApiDbContext; using TickAPI.Events.Abstractions; @@ -54,4 +55,24 @@ public async Task> GetEventByIdAsync(Guid eventId) return Result.Success(@event); } + + public async Task SaveEventAsync(Event ev) + { + var fromDb = await GetEventByIdAsync(ev.Id); + if (fromDb.IsError) + return Result.PropagateError(fromDb); + await _tickApiDbContext.SaveChangesAsync(); + return Result.Success(); + } + + public async Task> GetEventByIdAndOrganizerAsync(Guid eventId, Organizer organizer) + { + var organizerEvents = GetEventsByOranizer(organizer); + var ev = await organizerEvents.Where(e => e.Id == eventId).FirstAsync(); + if (ev is null) + { + return Result.Failure(StatusCodes.Status404NotFound, $"Event with id {eventId} not found for organizer with id {organizer.Id}"); + } + return Result.Success(ev); + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/Services/EventService.cs b/TickAPI/TickAPI/Events/Services/EventService.cs index 8177bd7..dbd3d0c 100644 --- a/TickAPI/TickAPI/Events/Services/EventService.cs +++ b/TickAPI/TickAPI/Events/Services/EventService.cs @@ -4,6 +4,7 @@ using TickAPI.Common.Pagination.Responses; using TickAPI.Categories.Abstractions; using TickAPI.Categories.DTOs.Request; +using TickAPI.Common.Results; using TickAPI.Common.Time.Abstractions; using TickAPI.Events.Abstractions; using TickAPI.Events.Models; @@ -40,32 +41,31 @@ public EventService(IEventRepository eventRepository, IOrganizerService organize _ticketService = ticketService; } - public async Task> CreateNewEventAsync(string name, string description, DateTime startDate, DateTime endDate, + public async Task> CreateNewEventAsync(string name, string description, DateTime startDate, DateTime endDate, uint? minimumAge, CreateAddressDto createAddress, List categories, List ticketTypes, EventStatus eventStatus, string organizerEmail) { var organizerResult = await _organizerService.GetOrganizerByEmailAsync(organizerEmail); if (!organizerResult.IsSuccess) return Result.PropagateError(organizerResult); - - if (endDate < startDate) - return Result.Failure(StatusCodes.Status400BadRequest, "End date should be after start date"); + var datesCheck = CheckEventDates(startDate, endDate); + if (datesCheck.IsError) + return Result.PropagateError(datesCheck); - if (startDate < _dateTimeService.GetCurrentDateTime()) - return Result.Failure(StatusCodes.Status400BadRequest, "Start date is in the past"); - if (ticketTypes.Any(t => t.AvailableFrom > endDate)) { return Result.Failure(StatusCodes.Status400BadRequest, "Tickets can't be available after the event is over"); } var address = await _addressService.GetOrCreateAddressAsync(createAddress); - + if (address.IsError) + { + return Result.PropagateError(address); + } + var categoryNames = categories.Select(c => c.CategoryName).ToList(); - var categoriesByNameResult = _categoryService.GetCategoriesByNames(categoryNames); - if (categoriesByNameResult.IsError) { return Result.PropagateError(categoriesByNameResult); @@ -163,6 +163,51 @@ public async Task> GetEventDetailsAsync(Guid return Result.Success(details); } + public async Task> EditEventAsync(Organizer organizer, Guid eventId, string name, string description, DateTime startDate, DateTime endDate, uint? minimumAge, CreateAddressDto editAddress, List categories, + EventStatus eventStatus) + { + var existingEventResult = await _eventRepository.GetEventByIdAndOrganizerAsync(eventId, organizer); + if (existingEventResult.IsError) + { + return existingEventResult; + } + var existingEvent = existingEventResult.Value!; + + var datesCheck = CheckEventDates(startDate, endDate); + if (datesCheck.IsError) + return Result.PropagateError(datesCheck); + + var address = await _addressService.GetOrCreateAddressAsync(editAddress); + if (address.IsError) + { + return Result.PropagateError(address); + } + + var categoryNames = categories.Select(c => c.CategoryName).ToList(); + var categoriesByNameResult = _categoryService.GetCategoriesByNames(categoryNames); + if (categoriesByNameResult.IsError) + { + return Result.PropagateError(categoriesByNameResult); + } + + existingEvent.Name = name; + existingEvent.Description = description; + existingEvent.StartDate = startDate; + existingEvent.EndDate = endDate; + existingEvent.MinimumAge = minimumAge; + existingEvent.Address = address.Value!; + existingEvent.Categories = categoriesByNameResult.Value!; + existingEvent.EventStatus = eventStatus; + + var saveResult = await _eventRepository.SaveEventAsync(existingEvent); + if (saveResult.IsError) + { + return Result.PropagateError(saveResult); + } + + return Result.Success(existingEvent); + } + private async Task>> GetPaginatedEventsAsync(IQueryable events, int page, int pageSize) { var paginatedEventsResult = await _paginationService.PaginateAsync(events, pageSize, page); @@ -203,4 +248,15 @@ private static GetEventResponseDto MapEventToGetEventResponseDto(Event ev) return new GetEventResponseDto(ev.Id, ev.Name, ev.Description, ev.StartDate, ev.EndDate, ev.MinimumAge, minimumPrice, maximumPrice, categories, ev.EventStatus, address); } + + private Result CheckEventDates(DateTime startDate, DateTime endDate) + { + if (endDate < startDate) + return Result.Failure(StatusCodes.Status400BadRequest, "End date should be after start date"); + + if (startDate < _dateTimeService.GetCurrentDateTime()) + return Result.Failure(StatusCodes.Status400BadRequest, "Start date is in the past"); + + return Result.Success(); + } } \ No newline at end of file From a0b879a46ba08bdda0b10f107057b0ed87ea015f Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Sun, 11 May 2025 15:02:08 +0200 Subject: [PATCH 43/80] Add tests for `EventService:EditEvenAsync` --- .../Events/Services/EventServiceTests.cs | 622 ++++++++++++++++++ 1 file changed, 622 insertions(+) diff --git a/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs b/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs index cda1b68..5f87144 100644 --- a/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs @@ -9,6 +9,7 @@ using TickAPI.Categories.Abstractions; using TickAPI.Categories.DTOs.Request; using TickAPI.Categories.Models; +using TickAPI.Common.Results; using TickAPI.Events.Models; using TickAPI.Organizers.Abstractions; using TickAPI.Organizers.Models; @@ -683,4 +684,625 @@ public async Task GetEventDetailsAsync_WhenFails_ShouldReturnEventError() Assert.Equal(StatusCodes.Status404NotFound, result.StatusCode); Assert.Equal($"event with id {@event.Id} not found", result.ErrorMsg); } + + [Fact] + public async Task EditEventAsync_WhenDataValid_ShouldUpdateEvent() + { + // Arrange + var organizer = new Organizer + { + Email = "organizer@example.com", + IsVerified = true + }; + var eventId = Guid.NewGuid(); + string name = "Updated Concert"; + string description = "Updated description of a concert"; + DateTime startDate = new DateTime(2025, 6, 1); + DateTime endDate = new DateTime(2025, 7, 1); + uint? minimumAge = 21; + EventStatus eventStatus = EventStatus.SoldOut; + + var existingEvent = new Event + { + Id = eventId, + Name = "Original Concert", + Description = "Original description", + StartDate = new DateTime(2025, 5, 1), + EndDate = new DateTime(2025, 5, 15), + MinimumAge = 18, + EventStatus = EventStatus.TicketsAvailable, + Organizer = organizer, + Categories = new List + { + new Category { Name = "rock" } + }, + Address = new Address + { + Country = "United States", + City = "Chicago", + Street = "State st", + HouseNumber = 100, + FlatNumber = null, + PostalCode = "60001" + } + }; + + List categories = + [ + new EditEventCategoryDto("jazz"), + new EditEventCategoryDto("classical") + ]; + + List expectedCategories = + [ + new Category { Name = "jazz" }, + new Category { Name = "classical" } + ]; + + CreateAddressDto editAddress = new CreateAddressDto("United States", "New York", "Broadway", 42, null, "10001"); + var updatedAddress = new Address + { + Country = "United States", + City = "New York", + Street = "Broadway", + HouseNumber = 42, + FlatNumber = null, + PostalCode = "10001" + }; + + var eventRepositoryMock = new Mock(); + eventRepositoryMock + .Setup(e => e.GetEventByIdAndOrganizerAsync(eventId, organizer)) + .ReturnsAsync(Result.Success(existingEvent)); + eventRepositoryMock + .Setup(e => e.SaveEventAsync(It.IsAny())) + .ReturnsAsync(Result.Success()); + + var organizerServiceMock = new Mock(); + + var addressServiceMock = new Mock(); + addressServiceMock + .Setup(m => m.GetOrCreateAddressAsync(editAddress)) + .ReturnsAsync(Result
.Success(updatedAddress)); + + var dateTimeServiceMock = new Mock(); + dateTimeServiceMock + .Setup(m => m.GetCurrentDateTime()) + .Returns(new DateTime(2025, 4, 15)); + + var categoryServiceMock = new Mock(); + categoryServiceMock + .Setup(c => c.GetCategoriesByNames(It.IsAny>())) + .Returns(Result>.Success(expectedCategories)); + + var paginationServiceMock = new Mock(); + var ticketServiceMock = new Mock(); + + var sut = new EventService( + eventRepositoryMock.Object, + organizerServiceMock.Object, + addressServiceMock.Object, + dateTimeServiceMock.Object, + paginationServiceMock.Object, + categoryServiceMock.Object, + ticketServiceMock.Object); + + // Act + var result = await sut.EditEventAsync( + organizer, + eventId, + name, + description, + startDate, + endDate, + minimumAge, + editAddress, + categories, + eventStatus); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(name, result.Value!.Name); + Assert.Equal(description, result.Value!.Description); + Assert.Equal(startDate, result.Value!.StartDate); + Assert.Equal(endDate, result.Value!.EndDate); + Assert.Equal(minimumAge, result.Value!.MinimumAge); + Assert.Equal(eventStatus, result.Value!.EventStatus); + Assert.Equal(expectedCategories.Count, result.Value!.Categories.Count); + Assert.Equal(updatedAddress, result.Value!.Address); + + // Verify repository was called to save the updated event + eventRepositoryMock.Verify(e => e.SaveEventAsync(It.IsAny()), Times.Once); + } + + [Fact] + public async Task EditEventAsync_WhenEventNotFound_ShouldReturnError() + { + // Arrange + var organizer = new Organizer + { + Email = "organizer@example.com", + IsVerified = true + }; + var eventId = Guid.NewGuid(); + string name = "Updated Concert"; + string description = "Updated description"; + DateTime startDate = new DateTime(2025, 6, 1); + DateTime endDate = new DateTime(2025, 7, 1); + uint? minimumAge = 21; + EventStatus eventStatus = EventStatus.SoldOut; + + List categories = + [ + new EditEventCategoryDto("jazz"), + new EditEventCategoryDto("classical") + ]; + + CreateAddressDto editAddress = new CreateAddressDto("United States", "New York", "Broadway", 42, null, "10001"); + + var eventRepositoryMock = new Mock(); + eventRepositoryMock + .Setup(e => e.GetEventByIdAndOrganizerAsync(eventId, organizer)) + .ReturnsAsync(Result.Failure(StatusCodes.Status404NotFound, "Event not found or not owned by organizer")); + + var organizerServiceMock = new Mock(); + var addressServiceMock = new Mock(); + var dateTimeServiceMock = new Mock(); + var categoryServiceMock = new Mock(); + var paginationServiceMock = new Mock(); + var ticketServiceMock = new Mock(); + + var sut = new EventService( + eventRepositoryMock.Object, + organizerServiceMock.Object, + addressServiceMock.Object, + dateTimeServiceMock.Object, + paginationServiceMock.Object, + categoryServiceMock.Object, + ticketServiceMock.Object); + + // Act + var result = await sut.EditEventAsync( + organizer, + eventId, + name, + description, + startDate, + endDate, + minimumAge, + editAddress, + categories, + eventStatus); + + // Assert + Assert.False(result.IsSuccess); + Assert.Equal(StatusCodes.Status404NotFound, result.StatusCode); + Assert.Equal("Event not found or not owned by organizer", result.ErrorMsg); + } + + [Fact] + public async Task EditEventAsync_WhenEndDateBeforeStartDate_ShouldReturnBadRequest() + { + // Arrange + var organizer = new Organizer + { + Email = "organizer@example.com", + IsVerified = true + }; + var eventId = Guid.NewGuid(); + string name = "Updated Concert"; + string description = "Updated description"; + DateTime startDate = new DateTime(2025, 7, 1); + DateTime endDate = new DateTime(2025, 6, 1); // End date before start date + uint? minimumAge = 21; + EventStatus eventStatus = EventStatus.SoldOut; + + var existingEvent = new Event + { + Id = eventId, + Name = "Original Concert", + Organizer = organizer + }; + + List categories = + [ + new EditEventCategoryDto("jazz") + ]; + + CreateAddressDto editAddress = new CreateAddressDto("United States", "New York", "Broadway", 42, null, "10001"); + + var eventRepositoryMock = new Mock(); + eventRepositoryMock + .Setup(e => e.GetEventByIdAndOrganizerAsync(eventId, organizer)) + .ReturnsAsync(Result.Success(existingEvent)); + + var organizerServiceMock = new Mock(); + var addressServiceMock = new Mock(); + + var dateTimeServiceMock = new Mock(); + dateTimeServiceMock + .Setup(m => m.GetCurrentDateTime()) + .Returns(new DateTime(2025, 4, 15)); + + var categoryServiceMock = new Mock(); + var paginationServiceMock = new Mock(); + var ticketServiceMock = new Mock(); + + var sut = new EventService( + eventRepositoryMock.Object, + organizerServiceMock.Object, + addressServiceMock.Object, + dateTimeServiceMock.Object, + paginationServiceMock.Object, + categoryServiceMock.Object, + ticketServiceMock.Object); + + // Act + var result = await sut.EditEventAsync( + organizer, + eventId, + name, + description, + startDate, + endDate, + minimumAge, + editAddress, + categories, + eventStatus); + + // Assert + Assert.False(result.IsSuccess); + Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); + Assert.Equal("End date should be after start date", result.ErrorMsg); + } + + [Fact] + public async Task EditEventAsync_WhenStartDateInPast_ShouldReturnBadRequest() + { + // Arrange + var organizer = new Organizer + { + Email = "organizer@example.com", + IsVerified = true + }; + var eventId = Guid.NewGuid(); + string name = "Updated Concert"; + string description = "Updated description"; + DateTime startDate = new DateTime(2025, 4, 1); // Start date in the past compared to current date + DateTime endDate = new DateTime(2025, 5, 1); + uint? minimumAge = 21; + EventStatus eventStatus = EventStatus.SoldOut; + + var existingEvent = new Event + { + Id = eventId, + Name = "Original Concert", + Organizer = organizer + }; + + List categories = + [ + new EditEventCategoryDto("jazz") + ]; + + CreateAddressDto editAddress = new CreateAddressDto("United States", "New York", "Broadway", 42, null, "10001"); + + var eventRepositoryMock = new Mock(); + eventRepositoryMock + .Setup(e => e.GetEventByIdAndOrganizerAsync(eventId, organizer)) + .ReturnsAsync(Result.Success(existingEvent)); + + var organizerServiceMock = new Mock(); + var addressServiceMock = new Mock(); + + var dateTimeServiceMock = new Mock(); + dateTimeServiceMock + .Setup(m => m.GetCurrentDateTime()) + .Returns(new DateTime(2025, 4, 15)); // Current date is after start date + + var categoryServiceMock = new Mock(); + var paginationServiceMock = new Mock(); + var ticketServiceMock = new Mock(); + + var sut = new EventService( + eventRepositoryMock.Object, + organizerServiceMock.Object, + addressServiceMock.Object, + dateTimeServiceMock.Object, + paginationServiceMock.Object, + categoryServiceMock.Object, + ticketServiceMock.Object); + + // Act + var result = await sut.EditEventAsync( + organizer, + eventId, + name, + description, + startDate, + endDate, + minimumAge, + editAddress, + categories, + eventStatus); + + // Assert + Assert.False(result.IsSuccess); + Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); + Assert.Equal("Start date is in the past", result.ErrorMsg); + } + + [Fact] + public async Task EditEventAsync_WhenAddressServiceFails_ShouldPropagateError() + { + // Arrange + var organizer = new Organizer + { + Email = "organizer@example.com", + IsVerified = true + }; + var eventId = Guid.NewGuid(); + string name = "Updated Concert"; + string description = "Updated description"; + DateTime startDate = new DateTime(2025, 6, 1); + DateTime endDate = new DateTime(2025, 7, 1); + uint? minimumAge = 21; + EventStatus eventStatus = EventStatus.SoldOut; + + var existingEvent = new Event + { + Id = eventId, + Name = "Original Concert", + Organizer = organizer + }; + + List categories = + [ + new EditEventCategoryDto("jazz") + ]; + + CreateAddressDto editAddress = new CreateAddressDto("", "", "", 0, null, ""); // Invalid address + + var eventRepositoryMock = new Mock(); + eventRepositoryMock + .Setup(e => e.GetEventByIdAndOrganizerAsync(eventId, organizer)) + .ReturnsAsync(Result.Success(existingEvent)); + + var organizerServiceMock = new Mock(); + + var addressServiceMock = new Mock(); + addressServiceMock + .Setup(m => m.GetOrCreateAddressAsync(editAddress)) + .ReturnsAsync(Result
.Failure(StatusCodes.Status400BadRequest, "Invalid address data")); + + var dateTimeServiceMock = new Mock(); + dateTimeServiceMock + .Setup(m => m.GetCurrentDateTime()) + .Returns(new DateTime(2025, 4, 15)); + + var categoryServiceMock = new Mock(); + var paginationServiceMock = new Mock(); + var ticketServiceMock = new Mock(); + + var sut = new EventService( + eventRepositoryMock.Object, + organizerServiceMock.Object, + addressServiceMock.Object, + dateTimeServiceMock.Object, + paginationServiceMock.Object, + categoryServiceMock.Object, + ticketServiceMock.Object); + + // Act + var result = await sut.EditEventAsync( + organizer, + eventId, + name, + description, + startDate, + endDate, + minimumAge, + editAddress, + categories, + eventStatus); + + // Assert + Assert.False(result.IsSuccess); + Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); + Assert.Equal("Invalid address data", result.ErrorMsg); + } + + [Fact] + public async Task EditEventAsync_WhenCategoryServiceFails_ShouldPropagateError() + { + // Arrange + var organizer = new Organizer + { + Email = "organizer@example.com", + IsVerified = true + }; + var eventId = Guid.NewGuid(); + string name = "Updated Concert"; + string description = "Updated description"; + DateTime startDate = new DateTime(2025, 6, 1); + DateTime endDate = new DateTime(2025, 7, 1); + uint? minimumAge = 21; + EventStatus eventStatus = EventStatus.SoldOut; + + var existingEvent = new Event + { + Id = eventId, + Name = "Original Concert", + Organizer = organizer + }; + + List categories = + [ + new EditEventCategoryDto("non-existent-category") + ]; + + CreateAddressDto editAddress = new CreateAddressDto("United States", "New York", "Broadway", 42, null, "10001"); + var updatedAddress = new Address + { + Country = "United States", + City = "New York", + Street = "Broadway", + HouseNumber = 42, + FlatNumber = null, + PostalCode = "10001" + }; + + var eventRepositoryMock = new Mock(); + eventRepositoryMock + .Setup(e => e.GetEventByIdAndOrganizerAsync(eventId, organizer)) + .ReturnsAsync(Result.Success(existingEvent)); + + var organizerServiceMock = new Mock(); + + var addressServiceMock = new Mock(); + addressServiceMock + .Setup(m => m.GetOrCreateAddressAsync(editAddress)) + .ReturnsAsync(Result
.Success(updatedAddress)); + + var dateTimeServiceMock = new Mock(); + dateTimeServiceMock + .Setup(m => m.GetCurrentDateTime()) + .Returns(new DateTime(2025, 4, 15)); + + var categoryServiceMock = new Mock(); + categoryServiceMock + .Setup(c => c.GetCategoriesByNames(It.IsAny>())) + .Returns(Result>.Failure(StatusCodes.Status404NotFound, "One or more categories not found")); + + var paginationServiceMock = new Mock(); + var ticketServiceMock = new Mock(); + + var sut = new EventService( + eventRepositoryMock.Object, + organizerServiceMock.Object, + addressServiceMock.Object, + dateTimeServiceMock.Object, + paginationServiceMock.Object, + categoryServiceMock.Object, + ticketServiceMock.Object); + + // Act + var result = await sut.EditEventAsync( + organizer, + eventId, + name, + description, + startDate, + endDate, + minimumAge, + editAddress, + categories, + eventStatus); + + // Assert + Assert.False(result.IsSuccess); + Assert.Equal(StatusCodes.Status404NotFound, result.StatusCode); + Assert.Equal("One or more categories not found", result.ErrorMsg); + } + + [Fact] + public async Task EditEventAsync_WhenSaveEventFails_ShouldPropagateError() + { + // Arrange + var organizer = new Organizer + { + Email = "organizer@example.com", + IsVerified = true + }; + var eventId = Guid.NewGuid(); + string name = "Updated Concert"; + string description = "Updated description"; + DateTime startDate = new DateTime(2025, 6, 1); + DateTime endDate = new DateTime(2025, 7, 1); + uint? minimumAge = 21; + EventStatus eventStatus = EventStatus.SoldOut; + + var existingEvent = new Event + { + Id = eventId, + Name = "Original Concert", + Organizer = organizer + }; + + List categories = + [ + new EditEventCategoryDto("jazz") + ]; + + List expectedCategories = + [ + new Category { Name = "jazz" } + ]; + + CreateAddressDto editAddress = new CreateAddressDto("United States", "New York", "Broadway", 42, null, "10001"); + var updatedAddress = new Address + { + Country = "United States", + City = "New York", + Street = "Broadway", + HouseNumber = 42, + FlatNumber = null, + PostalCode = "10001" + }; + + var eventRepositoryMock = new Mock(); + eventRepositoryMock + .Setup(e => e.GetEventByIdAndOrganizerAsync(eventId, organizer)) + .ReturnsAsync(Result.Success(existingEvent)); + eventRepositoryMock + .Setup(e => e.SaveEventAsync(It.IsAny())) + .ReturnsAsync(Result.Failure(StatusCodes.Status500InternalServerError, "Database error occurred")); + + var organizerServiceMock = new Mock(); + + var addressServiceMock = new Mock(); + addressServiceMock + .Setup(m => m.GetOrCreateAddressAsync(editAddress)) + .ReturnsAsync(Result
.Success(updatedAddress)); + + var dateTimeServiceMock = new Mock(); + dateTimeServiceMock + .Setup(m => m.GetCurrentDateTime()) + .Returns(new DateTime(2025, 4, 15)); + + var categoryServiceMock = new Mock(); + categoryServiceMock + .Setup(c => c.GetCategoriesByNames(It.IsAny>())) + .Returns(Result>.Success(expectedCategories)); + + var paginationServiceMock = new Mock(); + var ticketServiceMock = new Mock(); + + var sut = new EventService( + eventRepositoryMock.Object, + organizerServiceMock.Object, + addressServiceMock.Object, + dateTimeServiceMock.Object, + paginationServiceMock.Object, + categoryServiceMock.Object, + ticketServiceMock.Object); + + // Act + var result = await sut.EditEventAsync( + organizer, + eventId, + name, + description, + startDate, + endDate, + minimumAge, + editAddress, + categories, + eventStatus); + + // Assert + Assert.False(result.IsSuccess); + Assert.Equal(StatusCodes.Status500InternalServerError, result.StatusCode); + Assert.Equal("Database error occurred", result.ErrorMsg); + } } \ No newline at end of file From 1554e2d03bce9e54ea6930cf30a9034808ccfc40 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Mon, 12 May 2025 10:04:16 +0200 Subject: [PATCH 44/80] fix \n --- TickAPI/TickAPI/Events/Services/EventService.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/TickAPI/TickAPI/Events/Services/EventService.cs b/TickAPI/TickAPI/Events/Services/EventService.cs index dbd3d0c..e232b20 100644 --- a/TickAPI/TickAPI/Events/Services/EventService.cs +++ b/TickAPI/TickAPI/Events/Services/EventService.cs @@ -63,7 +63,6 @@ public async Task> CreateNewEventAsync(string name, string descri { return Result.PropagateError(address); } - var categoryNames = categories.Select(c => c.CategoryName).ToList(); var categoriesByNameResult = _categoryService.GetCategoriesByNames(categoryNames); if (categoriesByNameResult.IsError) From 08f2bbb607afa10ee6b70bef5fc08a36e86566b3 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Mon, 12 May 2025 10:27:25 +0200 Subject: [PATCH 45/80] Update logic behind checking if dates are correct in edited event --- .../Events/Services/EventServiceTests.cs | 234 +++++++++++++++++- .../TickAPI/Events/Services/EventService.cs | 40 +-- 2 files changed, 247 insertions(+), 27 deletions(-) diff --git a/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs b/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs index 5f87144..34b23ca 100644 --- a/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs @@ -724,7 +724,8 @@ public async Task EditEventAsync_WhenDataValid_ShouldUpdateEvent() HouseNumber = 100, FlatNumber = null, PostalCode = "60001" - } + }, + TicketTypes = [], }; List categories = @@ -901,7 +902,8 @@ public async Task EditEventAsync_WhenEndDateBeforeStartDate_ShouldReturnBadReque { Id = eventId, Name = "Original Concert", - Organizer = organizer + Organizer = organizer, + TicketTypes = [], }; List categories = @@ -957,7 +959,7 @@ public async Task EditEventAsync_WhenEndDateBeforeStartDate_ShouldReturnBadReque } [Fact] - public async Task EditEventAsync_WhenStartDateInPast_ShouldReturnBadRequest() + public async Task EditEventAsync_WhenStartDateChangedAndInPast_ShouldReturnBadRequest() { // Arrange var organizer = new Organizer @@ -977,7 +979,9 @@ public async Task EditEventAsync_WhenStartDateInPast_ShouldReturnBadRequest() { Id = eventId, Name = "Original Concert", - Organizer = organizer + Organizer = organizer, + TicketTypes = [], + StartDate = new DateTime(2025, 3, 1), }; List categories = @@ -1032,6 +1036,219 @@ public async Task EditEventAsync_WhenStartDateInPast_ShouldReturnBadRequest() Assert.Equal("Start date is in the past", result.ErrorMsg); } + [Fact] + public async Task EditEventAsync_StartDateNotChangedAndInPast_ShouldUpdateEvent() + { + // Arrange + var organizer = new Organizer + { + Email = "organizer@example.com", + IsVerified = true + }; + var eventId = Guid.NewGuid(); + string name = "Updated Concert"; + string description = "Updated description of a concert"; + DateTime startDate = new DateTime(2020, 1, 1); + DateTime endDate = new DateTime(2025, 7, 1); + uint? minimumAge = 21; + EventStatus eventStatus = EventStatus.SoldOut; + + var existingEvent = new Event + { + Id = eventId, + Name = "Original Concert", + Description = "Original description", + StartDate = new DateTime(2020, 1, 1), + EndDate = new DateTime(2025, 5, 15), + MinimumAge = 18, + EventStatus = EventStatus.TicketsAvailable, + Organizer = organizer, + Categories = new List + { + new Category { Name = "rock" } + }, + Address = new Address + { + Country = "United States", + City = "Chicago", + Street = "State st", + HouseNumber = 100, + FlatNumber = null, + PostalCode = "60001" + }, + TicketTypes = [], + }; + + List categories = + [ + new EditEventCategoryDto("jazz"), + new EditEventCategoryDto("classical") + ]; + + List expectedCategories = + [ + new Category { Name = "jazz" }, + new Category { Name = "classical" } + ]; + + CreateAddressDto editAddress = new CreateAddressDto("United States", "New York", "Broadway", 42, null, "10001"); + var updatedAddress = new Address + { + Country = "United States", + City = "New York", + Street = "Broadway", + HouseNumber = 42, + FlatNumber = null, + PostalCode = "10001" + }; + + var eventRepositoryMock = new Mock(); + eventRepositoryMock + .Setup(e => e.GetEventByIdAndOrganizerAsync(eventId, organizer)) + .ReturnsAsync(Result.Success(existingEvent)); + eventRepositoryMock + .Setup(e => e.SaveEventAsync(It.IsAny())) + .ReturnsAsync(Result.Success()); + + var organizerServiceMock = new Mock(); + + var addressServiceMock = new Mock(); + addressServiceMock + .Setup(m => m.GetOrCreateAddressAsync(editAddress)) + .ReturnsAsync(Result
.Success(updatedAddress)); + + var dateTimeServiceMock = new Mock(); + dateTimeServiceMock + .Setup(m => m.GetCurrentDateTime()) + .Returns(new DateTime(2025, 4, 15)); + + var categoryServiceMock = new Mock(); + categoryServiceMock + .Setup(c => c.GetCategoriesByNames(It.IsAny>())) + .Returns(Result>.Success(expectedCategories)); + + var paginationServiceMock = new Mock(); + var ticketServiceMock = new Mock(); + + var sut = new EventService( + eventRepositoryMock.Object, + organizerServiceMock.Object, + addressServiceMock.Object, + dateTimeServiceMock.Object, + paginationServiceMock.Object, + categoryServiceMock.Object, + ticketServiceMock.Object); + + // Act + var result = await sut.EditEventAsync( + organizer, + eventId, + name, + description, + startDate, + endDate, + minimumAge, + editAddress, + categories, + eventStatus); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(name, result.Value!.Name); + Assert.Equal(description, result.Value!.Description); + Assert.Equal(startDate, result.Value!.StartDate); + Assert.Equal(endDate, result.Value!.EndDate); + Assert.Equal(minimumAge, result.Value!.MinimumAge); + Assert.Equal(eventStatus, result.Value!.EventStatus); + Assert.Equal(expectedCategories.Count, result.Value!.Categories.Count); + Assert.Equal(updatedAddress, result.Value!.Address); + + // Verify repository was called to save the updated event + eventRepositoryMock.Verify(e => e.SaveEventAsync(It.IsAny()), Times.Once); + } + + [Fact] + public async Task EditEventAsync_WhenTicketTypeAvailableFromAfterEndDate_ShouldReturnBadRequest() + { + // Arrange + var organizer = new Organizer + { + Email = "organizer@example.com", + IsVerified = true + }; + var eventId = Guid.NewGuid(); + string name = "Updated Concert"; + string description = "Updated description"; + DateTime startDate = new DateTime(2025, 4, 1); + DateTime endDate = new DateTime(2025, 5, 1); + uint? minimumAge = 21; + EventStatus eventStatus = EventStatus.SoldOut; + + var existingEvent = new Event + { + Id = eventId, + Name = "Original Concert", + Organizer = organizer, + TicketTypes = [ + new TicketType + { + AvailableFrom = new DateTime(3000, 12, 12), + } + ], + }; + + List categories = + [ + new EditEventCategoryDto("jazz") + ]; + + CreateAddressDto editAddress = new CreateAddressDto("United States", "New York", "Broadway", 42, null, "10001"); + + var eventRepositoryMock = new Mock(); + eventRepositoryMock + .Setup(e => e.GetEventByIdAndOrganizerAsync(eventId, organizer)) + .ReturnsAsync(Result.Success(existingEvent)); + + var organizerServiceMock = new Mock(); + var addressServiceMock = new Mock(); + + var dateTimeServiceMock = new Mock(); + dateTimeServiceMock + .Setup(m => m.GetCurrentDateTime()) + .Returns(new DateTime(2000, 4, 15)); + + var categoryServiceMock = new Mock(); + var paginationServiceMock = new Mock(); + var ticketServiceMock = new Mock(); + + var sut = new EventService( + eventRepositoryMock.Object, + organizerServiceMock.Object, + addressServiceMock.Object, + dateTimeServiceMock.Object, + paginationServiceMock.Object, + categoryServiceMock.Object, + ticketServiceMock.Object); + + // Act + var result = await sut.EditEventAsync( + organizer, + eventId, + name, + description, + startDate, + endDate, + minimumAge, + editAddress, + categories, + eventStatus); + + // Assert + Assert.False(result.IsSuccess); + Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); + Assert.Equal("Tickets can't be available after the event is over", result.ErrorMsg); + } + [Fact] public async Task EditEventAsync_WhenAddressServiceFails_ShouldPropagateError() { @@ -1053,7 +1270,8 @@ public async Task EditEventAsync_WhenAddressServiceFails_ShouldPropagateError() { Id = eventId, Name = "Original Concert", - Organizer = organizer + Organizer = organizer, + TicketTypes = [], }; List categories = @@ -1133,7 +1351,8 @@ public async Task EditEventAsync_WhenCategoryServiceFails_ShouldPropagateError() { Id = eventId, Name = "Original Concert", - Organizer = organizer + Organizer = organizer, + TicketTypes = [], }; List categories = @@ -1226,7 +1445,8 @@ public async Task EditEventAsync_WhenSaveEventFails_ShouldPropagateError() { Id = eventId, Name = "Original Concert", - Organizer = organizer + Organizer = organizer, + TicketTypes = [], }; List categories = diff --git a/TickAPI/TickAPI/Events/Services/EventService.cs b/TickAPI/TickAPI/Events/Services/EventService.cs index e232b20..39f7807 100644 --- a/TickAPI/TickAPI/Events/Services/EventService.cs +++ b/TickAPI/TickAPI/Events/Services/EventService.cs @@ -49,15 +49,20 @@ public async Task> CreateNewEventAsync(string name, string descri if (!organizerResult.IsSuccess) return Result.PropagateError(organizerResult); - var datesCheck = CheckEventDates(startDate, endDate); + var ticketTypesConverted = ticketTypes.Select(t => new TicketType + { + Description = t.Description, + AvailableFrom = t.AvailableFrom, + Currency = t.Currency, + MaxCount = t.MaxCount, + Price = t.Price, + }) + .ToList(); + + var datesCheck = CheckEventDates(startDate, endDate, ticketTypesConverted); if (datesCheck.IsError) return Result.PropagateError(datesCheck); - if (ticketTypes.Any(t => t.AvailableFrom > endDate)) - { - return Result.Failure(StatusCodes.Status400BadRequest, "Tickets can't be available after the event is over"); - } - var address = await _addressService.GetOrCreateAddressAsync(createAddress); if (address.IsError) { @@ -69,17 +74,7 @@ public async Task> CreateNewEventAsync(string name, string descri { return Result.PropagateError(categoriesByNameResult); } - - var ticketTypesConverted = ticketTypes.Select(t => new TicketType - { - Description = t.Description, - AvailableFrom = t.AvailableFrom, - Currency = t.Currency, - MaxCount = t.MaxCount, - Price = t.Price, - }) - .ToList(); - + var @event = new Event { Name = name, @@ -172,7 +167,7 @@ public async Task> EditEventAsync(Organizer organizer, Guid eventI } var existingEvent = existingEventResult.Value!; - var datesCheck = CheckEventDates(startDate, endDate); + var datesCheck = CheckEventDates(startDate, endDate, existingEvent.TicketTypes, existingEvent.StartDate == startDate); if (datesCheck.IsError) return Result.PropagateError(datesCheck); @@ -248,13 +243,18 @@ private static GetEventResponseDto MapEventToGetEventResponseDto(Event ev) minimumPrice, maximumPrice, categories, ev.EventStatus, address); } - private Result CheckEventDates(DateTime startDate, DateTime endDate) + private Result CheckEventDates(DateTime startDate, DateTime endDate, IEnumerable ticketTypes, bool skipStartDateEvaluation = false) { if (endDate < startDate) return Result.Failure(StatusCodes.Status400BadRequest, "End date should be after start date"); - if (startDate < _dateTimeService.GetCurrentDateTime()) + if (!skipStartDateEvaluation && startDate < _dateTimeService.GetCurrentDateTime()) return Result.Failure(StatusCodes.Status400BadRequest, "Start date is in the past"); + + if (ticketTypes.Any(t => t.AvailableFrom > endDate)) + { + return Result.Failure(StatusCodes.Status400BadRequest, "Tickets can't be available after the event is over"); + } return Result.Success(); } From 51f54e6550281515e16d7b8a9141c82532b7cf2a Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Tue, 13 May 2025 18:51:53 +0200 Subject: [PATCH 46/80] Make `LastName` nullable in `AboutMeOrganizerResponseDto` --- .../Organizers/DTOs/Response/AboutMeOrganizerResponseDto.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TickAPI/TickAPI/Organizers/DTOs/Response/AboutMeOrganizerResponseDto.cs b/TickAPI/TickAPI/Organizers/DTOs/Response/AboutMeOrganizerResponseDto.cs index 5928d16..b7a983c 100644 --- a/TickAPI/TickAPI/Organizers/DTOs/Response/AboutMeOrganizerResponseDto.cs +++ b/TickAPI/TickAPI/Organizers/DTOs/Response/AboutMeOrganizerResponseDto.cs @@ -3,7 +3,7 @@ public record AboutMeOrganizerResponseDto( string Email, string FirstName, - string LastName, + string? LastName, string DisplayName, bool IsVerified, DateTime CreationDate From cf2fadab59e618e406208eba3c418b66419c7230 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Tue, 13 May 2025 18:53:25 +0200 Subject: [PATCH 47/80] Add endpoint for getting unverified organizers --- .../Abstractions/IOrganizerRepository.cs | 1 + .../Abstractions/IOrganizerService.cs | 6 ++++- .../Controllers/OrganizersController.cs | 9 ++++++++ .../GetUnverifiedOrganizerResponseDto.cs | 8 +++++++ .../Repositories/OrganizerRepository.cs | 5 +++++ .../Organizers/Services/OrganizerService.cs | 22 +++++++++++++++++-- 6 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 TickAPI/TickAPI/Organizers/DTOs/Response/GetUnverifiedOrganizerResponseDto.cs diff --git a/TickAPI/TickAPI/Organizers/Abstractions/IOrganizerRepository.cs b/TickAPI/TickAPI/Organizers/Abstractions/IOrganizerRepository.cs index 31b1c22..29513c6 100644 --- a/TickAPI/TickAPI/Organizers/Abstractions/IOrganizerRepository.cs +++ b/TickAPI/TickAPI/Organizers/Abstractions/IOrganizerRepository.cs @@ -9,4 +9,5 @@ public interface IOrganizerRepository Task> GetOrganizerByEmailAsync(string organizerEmail); Task AddNewOrganizerAsync(Organizer organizer); Task VerifyOrganizerByEmailAsync(string organizerEmail); + IQueryable GetOrganizers(); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Organizers/Abstractions/IOrganizerService.cs b/TickAPI/TickAPI/Organizers/Abstractions/IOrganizerService.cs index 0fb0bc6..226028f 100644 --- a/TickAPI/TickAPI/Organizers/Abstractions/IOrganizerService.cs +++ b/TickAPI/TickAPI/Organizers/Abstractions/IOrganizerService.cs @@ -1,5 +1,7 @@ -using TickAPI.Common.Results; +using TickAPI.Common.Pagination.Responses; +using TickAPI.Common.Results; using TickAPI.Common.Results.Generic; +using TickAPI.Organizers.DTOs.Response; using TickAPI.Organizers.Models; namespace TickAPI.Organizers.Abstractions; @@ -11,4 +13,6 @@ public interface IOrganizerService public Task> CreateNewOrganizerAsync(string email, string firstName, string lastName, string displayName); public Task VerifyOrganizerByEmailAsync(string organizerEmail); + + public Task>> GetUnverifiedOrganizersAsync(int page, int pageSize); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Organizers/Controllers/OrganizersController.cs b/TickAPI/TickAPI/Organizers/Controllers/OrganizersController.cs index efecb40..604ef1a 100644 --- a/TickAPI/TickAPI/Organizers/Controllers/OrganizersController.cs +++ b/TickAPI/TickAPI/Organizers/Controllers/OrganizersController.cs @@ -3,6 +3,7 @@ using TickAPI.Common.Auth.Attributes; using TickAPI.Common.Auth.Enums; using TickAPI.Common.Claims.Abstractions; +using TickAPI.Common.Pagination.Responses; using TickAPI.Common.Results.Generic; using TickAPI.Organizers.Abstractions; using TickAPI.Organizers.DTOs.Request; @@ -91,6 +92,14 @@ public async Task VerifyOrganizer([FromBody] VerifyOrganizerDto re return verifyOrganizerResult.ToObjectResult(); } + [AuthorizeWithPolicy(AuthPolicies.AdminPolicy)] + [HttpGet("unverified")] + public async Task>> GetUnverifiedOrganizers([FromQuery] int page, [FromQuery] int pageSize) + { + var result = await _organizerService.GetUnverifiedOrganizersAsync(page, pageSize); + return result.ToObjectResult(); + } + [AuthorizeWithPolicy(AuthPolicies.CreatedOrganizerPolicy)] [HttpGet("about-me")] public async Task> AboutMe() diff --git a/TickAPI/TickAPI/Organizers/DTOs/Response/GetUnverifiedOrganizerResponseDto.cs b/TickAPI/TickAPI/Organizers/DTOs/Response/GetUnverifiedOrganizerResponseDto.cs new file mode 100644 index 0000000..397fb29 --- /dev/null +++ b/TickAPI/TickAPI/Organizers/DTOs/Response/GetUnverifiedOrganizerResponseDto.cs @@ -0,0 +1,8 @@ +namespace TickAPI.Organizers.DTOs.Response; + +public record GetUnverifiedOrganizerResponseDto( + string Email, + string FirstName, + string? LastName, + string DisplayName +); diff --git a/TickAPI/TickAPI/Organizers/Repositories/OrganizerRepository.cs b/TickAPI/TickAPI/Organizers/Repositories/OrganizerRepository.cs index 1a01bce..6a43c22 100644 --- a/TickAPI/TickAPI/Organizers/Repositories/OrganizerRepository.cs +++ b/TickAPI/TickAPI/Organizers/Repositories/OrganizerRepository.cs @@ -53,4 +53,9 @@ public async Task VerifyOrganizerByEmailAsync(string organizerEmail) return Result.Success(); } + + public IQueryable GetOrganizers() + { + return _tickApiDbContext.Organizers; + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Organizers/Services/OrganizerService.cs b/TickAPI/TickAPI/Organizers/Services/OrganizerService.cs index 9ff72ce..75bbaf4 100644 --- a/TickAPI/TickAPI/Organizers/Services/OrganizerService.cs +++ b/TickAPI/TickAPI/Organizers/Services/OrganizerService.cs @@ -1,8 +1,11 @@ -using TickAPI.Common.Results; +using TickAPI.Common.Pagination.Abstractions; +using TickAPI.Common.Pagination.Responses; +using TickAPI.Common.Results; using TickAPI.Common.Results.Generic; using TickAPI.Common.Time.Abstractions; using TickAPI.Events.Models; using TickAPI.Organizers.Abstractions; +using TickAPI.Organizers.DTOs.Response; using TickAPI.Organizers.Models; namespace TickAPI.Organizers.Services; @@ -11,11 +14,13 @@ public class OrganizerService : IOrganizerService { private readonly IOrganizerRepository _organizerRepository; private readonly IDateTimeService _dateTimeService; + private readonly IPaginationService _paginationService; - public OrganizerService(IOrganizerRepository organizerRepository, IDateTimeService dateTimeService) + public OrganizerService(IOrganizerRepository organizerRepository, IDateTimeService dateTimeService, IPaginationService paginationService) { _organizerRepository = organizerRepository; _dateTimeService = dateTimeService; + _paginationService = paginationService; } public async Task> GetOrganizerByEmailAsync(string organizerEmail) @@ -48,4 +53,17 @@ public async Task VerifyOrganizerByEmailAsync(string organizerEmail) { return await _organizerRepository.VerifyOrganizerByEmailAsync(organizerEmail); } + + public async Task>> GetUnverifiedOrganizersAsync(int page, int pageSize) + { + var unverifiedOrganizers = _organizerRepository.GetOrganizers().Where(o => !o.IsVerified); + var paginatedResult = await _paginationService.PaginateAsync(unverifiedOrganizers, pageSize, page); + if (paginatedResult.IsError) + { + return Result>.PropagateError(paginatedResult); + } + var paginated = paginatedResult.Value!; + var mapped = _paginationService.MapData(paginated, (o) => new GetUnverifiedOrganizerResponseDto(o.Email, o.FirstName, o.LastName, o.DisplayName)); + return Result>.Success(mapped); + } } \ No newline at end of file From 4a71c77910c431c603ac363f30836c07fd957adb Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Tue, 13 May 2025 18:53:37 +0200 Subject: [PATCH 48/80] Fix already existing tests --- .../Services/OrganizerServiceTests.cs | 36 +++++++++++++++---- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/TickAPI/TickAPI.Tests/Organizers/Services/OrganizerServiceTests.cs b/TickAPI/TickAPI.Tests/Organizers/Services/OrganizerServiceTests.cs index 1e91aa9..b220395 100644 --- a/TickAPI/TickAPI.Tests/Organizers/Services/OrganizerServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Organizers/Services/OrganizerServiceTests.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Http; using Moq; +using TickAPI.Common.Pagination.Abstractions; using TickAPI.Common.Results; using TickAPI.Common.Results.Generic; using TickAPI.Common.Time.Abstractions; @@ -29,9 +30,12 @@ public async Task GetOrganizerByEmailAsync_WhenOrganizerWithEmailIsReturnedFromR var dateTimeServiceMock = new Mock(); + var paginationServiceMock = new Mock(); + var sut = new OrganizerService( organizerRepositoryMock.Object, - dateTimeServiceMock.Object + dateTimeServiceMock.Object, + paginationServiceMock.Object ); // Act @@ -55,9 +59,12 @@ public async Task GetOrganizerByEmailAsync_WhenOrganizerWithEmailIsNotReturnedFr var dateTimeServiceMock = new Mock(); + var paginationServiceMock = new Mock(); + var sut = new OrganizerService( organizerRepositoryMock.Object, - dateTimeServiceMock.Object + dateTimeServiceMock.Object, + paginationServiceMock.Object ); // Act @@ -94,9 +101,12 @@ public async Task CreateNewOrganizerAsync_WhenOrganizerDataIsValid_ShouldReturnN .Setup(m => m.GetCurrentDateTime()) .Returns(currentDate); + var paginationServiceMock = new Mock(); + var sut = new OrganizerService( organizerRepositoryMock.Object, - dateTimeServiceMock.Object + dateTimeServiceMock.Object, + paginationServiceMock.Object ); // Act @@ -138,9 +148,12 @@ public async Task CreateNewOrganizerAsync_WhenLastNameIsNull_ShouldReturnNewOrga .Setup(m => m.GetCurrentDateTime()) .Returns(currentDate); + var paginationServiceMock = new Mock(); + var sut = new OrganizerService( organizerRepositoryMock.Object, - dateTimeServiceMock.Object + dateTimeServiceMock.Object, + paginationServiceMock.Object ); // Act @@ -173,9 +186,12 @@ public async Task CreateNewOrganizerAsync_WhenWithNotUniqueEmail_ShouldReturnFai var dateTimeServiceMock = new Mock(); + var paginationServiceMock = new Mock(); + var sut = new OrganizerService( organizerRepositoryMock.Object, - dateTimeServiceMock.Object + dateTimeServiceMock.Object, + paginationServiceMock.Object ); // Act @@ -199,9 +215,12 @@ public async Task VerifyOrganizerByEmailAsync_WhenVerificationSuccessful_ShouldR var dateTimeServiceMock = new Mock(); + var paginationServiceMock = new Mock(); + var sut = new OrganizerService( organizerRepositoryMock.Object, - dateTimeServiceMock.Object + dateTimeServiceMock.Object, + paginationServiceMock.Object ); // Act @@ -223,9 +242,12 @@ public async Task VerifyOrganizerByEmailAsync_WhenVerificationNotSuccessful_Shou var dateTimeServiceMock = new Mock(); + var paginationServiceMock = new Mock(); + var sut = new OrganizerService( organizerRepositoryMock.Object, - dateTimeServiceMock.Object + dateTimeServiceMock.Object, + paginationServiceMock.Object ); // Act From 10f2884deeb4bb1b97a374d20a87bbb4c414f3a4 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Tue, 13 May 2025 19:13:58 +0200 Subject: [PATCH 49/80] Add tests for `GetUnverifiedOrganizersAsync` --- .../Services/OrganizerServiceTests.cs | 301 ++++++++++++++++++ 1 file changed, 301 insertions(+) diff --git a/TickAPI/TickAPI.Tests/Organizers/Services/OrganizerServiceTests.cs b/TickAPI/TickAPI.Tests/Organizers/Services/OrganizerServiceTests.cs index b220395..e517360 100644 --- a/TickAPI/TickAPI.Tests/Organizers/Services/OrganizerServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Organizers/Services/OrganizerServiceTests.cs @@ -1,10 +1,12 @@ using Microsoft.AspNetCore.Http; using Moq; using TickAPI.Common.Pagination.Abstractions; +using TickAPI.Common.Pagination.Responses; using TickAPI.Common.Results; using TickAPI.Common.Results.Generic; using TickAPI.Common.Time.Abstractions; using TickAPI.Organizers.Abstractions; +using TickAPI.Organizers.DTOs.Response; using TickAPI.Organizers.Models; using TickAPI.Organizers.Services; @@ -258,4 +260,303 @@ public async Task VerifyOrganizerByEmailAsync_WhenVerificationNotSuccessful_Shou Assert.Equal(StatusCodes.Status404NotFound, result.StatusCode); Assert.Equal($"organizer with email '{email}' not found", result.ErrorMsg); } + + [Fact] + public async Task GetUnverifiedOrganizersAsync_WhenPaginationSuccessful_ShouldReturnPaginatedUnverifiedOrganizers() + { + // Arrange + const int page = 0; + const int pageSize = 10; + + var unverifiedOrganizers = new List + { + new() { Email = "unverified1@test.com", FirstName = "First1", LastName = "Last1", DisplayName = "Display1", IsVerified = false }, + new() { Email = "unverified2@test.com", FirstName = "First2", LastName = "Last2", DisplayName = "Display2", IsVerified = false }, + new() { Email = "unverified3@test.com", FirstName = "First3", LastName = "Last3", DisplayName = "Display3", IsVerified = false } + }.AsQueryable(); + + var paginationDetails = new PaginationDetails(0, 3); + var paginatedData = new PaginatedData( + unverifiedOrganizers.ToList(), + page, + pageSize, + false, + false, + paginationDetails + ); + + var expectedDtos = new List + { + new("unverified1@test.com", "First1", "Last1", "Display1"), + new("unverified2@test.com", "First2", "Last2", "Display2"), + new("unverified3@test.com", "First3", "Last3", "Display3") + }; + + var mappedData = new PaginatedData( + expectedDtos, + page, + pageSize, + false, + false, + paginationDetails + ); + + var organizerRepositoryMock = new Mock(); + organizerRepositoryMock + .Setup(m => m.GetOrganizers()) + .Returns(unverifiedOrganizers); + + var dateTimeServiceMock = new Mock(); + + var paginationServiceMock = new Mock(); + paginationServiceMock + .Setup(m => m.PaginateAsync(unverifiedOrganizers, pageSize, page)) + .ReturnsAsync(Result>.Success(paginatedData)); + + // Capture and verify the mapping function + Func capturedMapFunction = null; + paginationServiceMock + .Setup(m => m.MapData(paginatedData, It.IsAny>())) + .Returns, Func>((source, mapFunc) => + { + capturedMapFunction = mapFunc; + return mappedData; + }); + + var sut = new OrganizerService( + organizerRepositoryMock.Object, + dateTimeServiceMock.Object, + paginationServiceMock.Object + ); + + // Act + var result = await sut.GetUnverifiedOrganizersAsync(page, pageSize); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(mappedData, result.Value); + Assert.Equal(3, result.Value!.Data.Count); + + // Verify each DTO was correctly mapped + for (int i = 0; i < expectedDtos.Count; i++) + { + Assert.Equal(expectedDtos[i], result.Value.Data[i]); + } + + // Verify the mapping function works correctly + Assert.NotNull(capturedMapFunction); + var testOrganizer = new Organizer { Email = "test@example.com", FirstName = "TestFirst", LastName = "TestLast", DisplayName = "TestDisplay" }; + var mappedDto = capturedMapFunction(testOrganizer); + var expectedDto = new GetUnverifiedOrganizerResponseDto("test@example.com", "TestFirst", "TestLast", "TestDisplay"); + Assert.Equal(expectedDto, mappedDto); + } + + [Fact] + public async Task GetUnverifiedOrganizersAsync_WhenFilteringOrganizers_ShouldOnlyReturnUnverifiedOnes() + { + // Arrange + const int page = 0; + const int pageSize = 10; + + var mixedOrganizers = new List + { + new() { Email = "unverified1@test.com", FirstName = "First1", LastName = "Last1", DisplayName = "Display1", IsVerified = false }, + new() { Email = "verified1@test.com", FirstName = "First2", LastName = "Last2", DisplayName = "Display2", IsVerified = true }, + new() { Email = "unverified2@test.com", FirstName = "First3", LastName = "Last3", DisplayName = "Display3", IsVerified = false } + }.AsQueryable(); + + var filteredOrganizers = mixedOrganizers.Where(o => !o.IsVerified).ToList(); + + var paginationDetails = new PaginationDetails(0, 2); + var paginatedData = new PaginatedData( + filteredOrganizers, + page, + pageSize, + false, + false, + paginationDetails + ); + + var expectedDtos = new List + { + new("unverified1@test.com", "First1", "Last1", "Display1"), + new("unverified2@test.com", "First3", "Last3", "Display3") + }; + + var mappedData = new PaginatedData( + expectedDtos, + page, + pageSize, + false, + false, + paginationDetails + ); + + var organizerRepositoryMock = new Mock(); + organizerRepositoryMock + .Setup(m => m.GetOrganizers()) + .Returns(mixedOrganizers); + + var dateTimeServiceMock = new Mock(); + + var paginationServiceMock = new Mock(); + paginationServiceMock + .Setup(m => m.PaginateAsync(It.Is>(q => q.Count() == 2), pageSize, page)) + .ReturnsAsync(Result>.Success(paginatedData)); + paginationServiceMock + .Setup(m => m.MapData(paginatedData, It.IsAny>())) + .Returns(mappedData); + + var sut = new OrganizerService( + organizerRepositoryMock.Object, + dateTimeServiceMock.Object, + paginationServiceMock.Object + ); + + // Act + var result = await sut.GetUnverifiedOrganizersAsync(page, pageSize); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(mappedData, result.Value); + Assert.Equal(2, result.Value!.Data.Count); + Assert.Equal(expectedDtos, result.Value.Data); + Assert.DoesNotContain(result.Value.Data, dto => dto.Email == "verified1@test.com"); + } + + [Fact] + public async Task GetUnverifiedOrganizersAsync_WhenNoPaginationResults_ShouldReturnEmptyList() + { + // Arrange + const int page = 0; + const int pageSize = 10; + + var emptyOrganizers = new List().AsQueryable(); + + var paginationDetails = new PaginationDetails(0, 0); + var paginatedData = new PaginatedData( + new List(), + page, + pageSize, + false, + false, + paginationDetails + ); + + var mappedData = new PaginatedData( + new List(), + page, + pageSize, + false, + false, + paginationDetails + ); + + var organizerRepositoryMock = new Mock(); + organizerRepositoryMock + .Setup(m => m.GetOrganizers()) + .Returns(emptyOrganizers); + + var dateTimeServiceMock = new Mock(); + + var paginationServiceMock = new Mock(); + paginationServiceMock + .Setup(m => m.PaginateAsync(It.IsAny>(), pageSize, page)) + .ReturnsAsync(Result>.Success(paginatedData)); + paginationServiceMock + .Setup(m => m.MapData(paginatedData, It.IsAny>())) + .Returns(mappedData); + + var sut = new OrganizerService( + organizerRepositoryMock.Object, + dateTimeServiceMock.Object, + paginationServiceMock.Object + ); + + // Act + var result = await sut.GetUnverifiedOrganizersAsync(page, pageSize); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(mappedData, result.Value); + Assert.Empty(result.Value!.Data); + } + + [Fact] + public async Task GetUnverifiedOrganizersAsync_WithNullLastNames_ShouldMapCorrectly() + { + // Arrange + const int page = 0; + const int pageSize = 10; + + var unverifiedOrganizers = new List + { + new() { Email = "nulllast@test.com", FirstName = "First", LastName = null, DisplayName = "Display", IsVerified = false }, + }.AsQueryable(); + + var paginationDetails = new PaginationDetails(0, 1); + var paginatedData = new PaginatedData( + unverifiedOrganizers.ToList(), + page, + pageSize, + false, + false, + paginationDetails + ); + + var expectedDto = new GetUnverifiedOrganizerResponseDto("nulllast@test.com", "First", null, "Display"); + var mappedData = new PaginatedData( + new List { expectedDto }, + page, + pageSize, + false, + false, + paginationDetails + ); + + var organizerRepositoryMock = new Mock(); + organizerRepositoryMock + .Setup(m => m.GetOrganizers()) + .Returns(unverifiedOrganizers); + + var dateTimeServiceMock = new Mock(); + + var paginationServiceMock = new Mock(); + paginationServiceMock + .Setup(m => m.PaginateAsync(It.IsAny>(), pageSize, page)) + .ReturnsAsync(Result>.Success(paginatedData)); + + // Verify the mapping function handles null LastName correctly + Func capturedMapFunction = null; + paginationServiceMock + .Setup(m => m.MapData(paginatedData, It.IsAny>())) + .Returns, Func>((source, mapFunc) => + { + capturedMapFunction = mapFunc; + return mappedData; + }); + + var sut = new OrganizerService( + organizerRepositoryMock.Object, + dateTimeServiceMock.Object, + paginationServiceMock.Object + ); + + // Act + var result = await sut.GetUnverifiedOrganizersAsync(page, pageSize); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(mappedData, result.Value); + Assert.Single(result.Value!.Data); + Assert.Equal(expectedDto, result.Value.Data[0]); + Assert.Null(result.Value.Data[0].LastName); + + // Verify the mapping function works correctly with null LastName + Assert.NotNull(capturedMapFunction); + var testOrganizer = new Organizer { Email = "test@example.com", FirstName = "TestFirst", LastName = null, DisplayName = "TestDisplay" }; + var mappedDto = capturedMapFunction(testOrganizer); + var expectedMappedDto = new GetUnverifiedOrganizerResponseDto("test@example.com", "TestFirst", null, "TestDisplay"); + Assert.Equal(expectedMappedDto, mappedDto); + } } \ No newline at end of file From 7f5f88a70e2fc3542026ea40d9c0480a96476d3b Mon Sep 17 00:00:00 2001 From: kubapoke Date: Thu, 15 May 2025 01:32:33 +0200 Subject: [PATCH 50/80] added getting tickets from cart --- .../Abstractions/IShoppingCartService.cs | 4 +++- .../GetShoppingCartTicketsResponseDto.cs | 8 ++++++++ .../ShoppingCarts/Models/ShoppingCart.cs | 2 +- .../Services/ShoppingCartService.cs | 18 +++++++++++++++--- TickAPI/TickAPI/TickAPI.csproj | 4 ---- 5 files changed, 27 insertions(+), 9 deletions(-) create mode 100644 TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsResponseDto.cs diff --git a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs index 12103bb..226d9f9 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs @@ -1,11 +1,13 @@ using TickAPI.Common.Results; +using TickAPI.Common.Results.Generic; +using TickAPI.ShoppingCarts.DTOs.Response; namespace TickAPI.ShoppingCarts.Abstractions; public interface IShoppingCartService { public Task AddNewTicketAsync(Guid ticketTypeId, string customerEmail, string? nameOnTicket, string? seats); - public Task GetTicketsAsync(); + public Task> GetTicketsAsync(string customerEmail); public Task RemoveTicketAsync(); public Task CheckoutAsync(); } \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsResponseDto.cs b/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsResponseDto.cs new file mode 100644 index 0000000..c4c3f96 --- /dev/null +++ b/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsResponseDto.cs @@ -0,0 +1,8 @@ +using TickAPI.Tickets.Models; + +namespace TickAPI.ShoppingCarts.DTOs.Response; + +public record GetShoppingCartTicketsResponseDto( + List NewTickets, + List ResellTickets +); \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Models/ShoppingCart.cs b/TickAPI/TickAPI/ShoppingCarts/Models/ShoppingCart.cs index 2ed9b5f..4d480eb 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Models/ShoppingCart.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Models/ShoppingCart.cs @@ -4,6 +4,6 @@ namespace TickAPI.ShoppingCarts.Models; public class ShoppingCart { - public List Tickets { get; set; } = []; + public List NewTickets { get; set; } = []; public List ResellTickets { get; set; } = []; } \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs index 8c89917..83e2421 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs @@ -1,5 +1,7 @@ using TickAPI.Common.Results; +using TickAPI.Common.Results.Generic; using TickAPI.ShoppingCarts.Abstractions; +using TickAPI.ShoppingCarts.DTOs.Response; using TickAPI.Tickets.Models; namespace TickAPI.ShoppingCarts.Services; @@ -24,7 +26,7 @@ public async Task AddNewTicketAsync(Guid ticketTypeId, string customerEm var cart = getShoppingCartResult.Value!; - cart.Tickets.Add(new ShoppingCartNewTicket() + cart.NewTickets.Add(new ShoppingCartNewTicket() { TicketTypeId = ticketTypeId, NameOnTicket = nameOnTicket, @@ -41,9 +43,19 @@ public async Task AddNewTicketAsync(Guid ticketTypeId, string customerEm return Result.Success(); } - public Task GetTicketsAsync() + public async Task> GetTicketsAsync(string customerEmail) { - throw new NotImplementedException(); + var getShoppingCartResult = await _shoppingCartRepository.GetShoppingCartByEmailAsync(customerEmail); + + if (getShoppingCartResult.IsError) + { + return Result.PropagateError(getShoppingCartResult); + } + + var cart = getShoppingCartResult.Value!; + var result = new GetShoppingCartTicketsResponseDto(cart.NewTickets, cart.ResellTickets); + + return Result.Success(result); } public Task RemoveTicketAsync() diff --git a/TickAPI/TickAPI/TickAPI.csproj b/TickAPI/TickAPI/TickAPI.csproj index ecbd7d6..fb79365 100644 --- a/TickAPI/TickAPI/TickAPI.csproj +++ b/TickAPI/TickAPI/TickAPI.csproj @@ -25,10 +25,6 @@ - - - - From 91fb5269ab5c4804c65712454ba55e993d5e25f3 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Thu, 15 May 2025 01:58:12 +0200 Subject: [PATCH 51/80] removed NameOnTicket being necessary --- ...01_MadeNameOnTicketUnnecessary.Designer.cs | 391 ++++++++++++++++++ ...50514235701_MadeNameOnTicketUnnecessary.cs | 36 ++ .../TickApiDbContextModelSnapshot.cs | 1 - TickAPI/TickAPI/Tickets/Models/Ticket.cs | 2 +- 4 files changed, 428 insertions(+), 2 deletions(-) create mode 100644 TickAPI/TickAPI/Migrations/20250514235701_MadeNameOnTicketUnnecessary.Designer.cs create mode 100644 TickAPI/TickAPI/Migrations/20250514235701_MadeNameOnTicketUnnecessary.cs diff --git a/TickAPI/TickAPI/Migrations/20250514235701_MadeNameOnTicketUnnecessary.Designer.cs b/TickAPI/TickAPI/Migrations/20250514235701_MadeNameOnTicketUnnecessary.Designer.cs new file mode 100644 index 0000000..e146523 --- /dev/null +++ b/TickAPI/TickAPI/Migrations/20250514235701_MadeNameOnTicketUnnecessary.Designer.cs @@ -0,0 +1,391 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using TickAPI.Common.TickApiDbContext; + +#nullable disable + +namespace TickAPI.Migrations +{ + [DbContext(typeof(TickApiDbContext))] + [Migration("20250514235701_MadeNameOnTicketUnnecessary")] + partial class MadeNameOnTicketUnnecessary + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("CategoryEvent", b => + { + b.Property("CategoriesId") + .HasColumnType("uniqueidentifier"); + + b.Property("EventsId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("CategoriesId", "EventsId"); + + b.HasIndex("EventsId"); + + b.ToTable("CategoryEvent"); + }); + + modelBuilder.Entity("TickAPI.Addresses.Models.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("City") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Country") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FlatNumber") + .HasColumnType("bigint"); + + b.Property("HouseNumber") + .HasColumnType("bigint"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Street") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Addresses"); + }); + + modelBuilder.Entity("TickAPI.Admins.Models.Admin", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Login") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Admins"); + }); + + modelBuilder.Entity("TickAPI.Categories.Models.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + + b.HasData( + new + { + Id = new Guid("ec3daf69-baa9-4fcd-a674-c09884a57272"), + Name = "Music" + }, + new + { + Id = new Guid("de89dd76-3b29-43e1-8f4b-5278b1b8bde2"), + Name = "Sports" + }, + new + { + Id = new Guid("ea58370b-2a17-4770-abea-66399ad69fb8"), + Name = "Conferences" + }, + new + { + Id = new Guid("4a086d9e-59de-4fd1-a1b2-bd9b5eec797c"), + Name = "Theatre" + }, + new + { + Id = new Guid("5f8dbe65-30be-453f-8f22-191a11b2977b"), + Name = "Comedy" + }, + new + { + Id = new Guid("4421327a-4bc8-4706-bec0-666f78ed0c69"), + Name = "Workshops" + }); + }); + + modelBuilder.Entity("TickAPI.Customers.Models.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Event", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AddressId") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("EventStatus") + .HasColumnType("int"); + + b.Property("MinimumAge") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OrganizerId") + .HasColumnType("uniqueidentifier"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("AddressId"); + + b.HasIndex("OrganizerId"); + + b.ToTable("Events"); + }); + + modelBuilder.Entity("TickAPI.Organizers.Models.Organizer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsVerified") + .HasColumnType("bit"); + + b.Property("LastName") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Organizers"); + }); + + modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AvailableFrom") + .HasColumnType("datetime2"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EventId") + .HasColumnType("uniqueidentifier"); + + b.Property("MaxCount") + .HasColumnType("bigint"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("EventId"); + + b.ToTable("TicketTypes"); + }); + + modelBuilder.Entity("TickAPI.Tickets.Models.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ForResell") + .HasColumnType("bit"); + + b.Property("NameOnTicket") + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("uniqueidentifier"); + + b.Property("Seats") + .HasColumnType("nvarchar(max)"); + + b.Property("TypeId") + .HasColumnType("uniqueidentifier"); + + b.Property("Used") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("TypeId"); + + b.ToTable("Tickets"); + }); + + modelBuilder.Entity("CategoryEvent", b => + { + b.HasOne("TickAPI.Categories.Models.Category", null) + .WithMany() + .HasForeignKey("CategoriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TickAPI.Events.Models.Event", null) + .WithMany() + .HasForeignKey("EventsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Event", b => + { + b.HasOne("TickAPI.Addresses.Models.Address", "Address") + .WithMany() + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TickAPI.Organizers.Models.Organizer", "Organizer") + .WithMany("Events") + .HasForeignKey("OrganizerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Address"); + + b.Navigation("Organizer"); + }); + + modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => + { + b.HasOne("TickAPI.Events.Models.Event", "Event") + .WithMany("TicketTypes") + .HasForeignKey("EventId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Event"); + }); + + modelBuilder.Entity("TickAPI.Tickets.Models.Ticket", b => + { + b.HasOne("TickAPI.Customers.Models.Customer", "Owner") + .WithMany("Tickets") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TickAPI.TicketTypes.Models.TicketType", "Type") + .WithMany("Tickets") + .HasForeignKey("TypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + + b.Navigation("Type"); + }); + + modelBuilder.Entity("TickAPI.Customers.Models.Customer", b => + { + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Event", b => + { + b.Navigation("TicketTypes"); + }); + + modelBuilder.Entity("TickAPI.Organizers.Models.Organizer", b => + { + b.Navigation("Events"); + }); + + modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => + { + b.Navigation("Tickets"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TickAPI/TickAPI/Migrations/20250514235701_MadeNameOnTicketUnnecessary.cs b/TickAPI/TickAPI/Migrations/20250514235701_MadeNameOnTicketUnnecessary.cs new file mode 100644 index 0000000..3cbba3e --- /dev/null +++ b/TickAPI/TickAPI/Migrations/20250514235701_MadeNameOnTicketUnnecessary.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TickAPI.Migrations +{ + /// + public partial class MadeNameOnTicketUnnecessary : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "NameOnTicket", + table: "Tickets", + type: "nvarchar(max)", + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(max)"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "NameOnTicket", + table: "Tickets", + type: "nvarchar(max)", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "nvarchar(max)", + oldNullable: true); + } + } +} diff --git a/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs b/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs index 50fd0cc..dc78ef4 100644 --- a/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs +++ b/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs @@ -276,7 +276,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("bit"); b.Property("NameOnTicket") - .IsRequired() .HasColumnType("nvarchar(max)"); b.Property("OwnerId") diff --git a/TickAPI/TickAPI/Tickets/Models/Ticket.cs b/TickAPI/TickAPI/Tickets/Models/Ticket.cs index 49a68ac..631f8db 100644 --- a/TickAPI/TickAPI/Tickets/Models/Ticket.cs +++ b/TickAPI/TickAPI/Tickets/Models/Ticket.cs @@ -8,7 +8,7 @@ public class Ticket public Guid Id { get; set; } public TicketType Type { get; set; } public Customer Owner { get; set; } - public string NameOnTicket { get; set; } + public string? NameOnTicket { get; set; } public string? Seats { get; set; } public bool ForResell { get; set; } public bool Used { get; set; } From c3222dff85d72142af07031eb4432a558a72fdab Mon Sep 17 00:00:00 2001 From: kubapoke Date: Thu, 15 May 2025 02:02:30 +0200 Subject: [PATCH 52/80] implemented the basic version of getting tickets --- .../Abstractions/IShoppingCartService.cs | 6 +++--- .../Controllers/ShoppingCartsController.cs | 20 ++++++++++++++++--- .../Services/ShoppingCartService.cs | 6 +++--- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs index 226d9f9..8bd2aee 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs @@ -6,8 +6,8 @@ namespace TickAPI.ShoppingCarts.Abstractions; public interface IShoppingCartService { - public Task AddNewTicketAsync(Guid ticketTypeId, string customerEmail, string? nameOnTicket, string? seats); - public Task> GetTicketsAsync(string customerEmail); - public Task RemoveTicketAsync(); + public Task AddNewTicketToCartAsync(Guid ticketTypeId, string customerEmail, string? nameOnTicket, string? seats); + public Task> GetTicketsFromCartAsync(string customerEmail); + public Task RemoveTicketFromCartAsync(); public Task CheckoutAsync(); } \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs b/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs index 3f3e16a..aca5cc2 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs @@ -4,6 +4,7 @@ using TickAPI.Common.Claims.Abstractions; using TickAPI.ShoppingCarts.Abstractions; using TickAPI.ShoppingCarts.DTOs.Request; +using TickAPI.ShoppingCarts.DTOs.Response; namespace TickAPI.ShoppingCarts.Controllers; @@ -31,7 +32,7 @@ public async Task AddTicket([FromBody] AddNewTicketDto addNewTicke } var email = emailResult.Value!; - var addTicketResult = await _shoppingCartService.AddNewTicketAsync(addNewTicketDto.TicketTypeId, email, + var addTicketResult = await _shoppingCartService.AddNewTicketToCartAsync(addNewTicketDto.TicketTypeId, email, addNewTicketDto.NameOnTicket, addNewTicketDto.Seats); if (addTicketResult.IsError) { @@ -43,9 +44,22 @@ public async Task AddTicket([FromBody] AddNewTicketDto addNewTicke [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] [HttpGet("tickets")] - public async Task GetTickets() + public async Task> GetTickets() { - throw new NotImplementedException(); + var emailResult = _claimsService.GetEmailFromClaims(User.Claims); + if (emailResult.IsError) + { + return StatusCode(emailResult.StatusCode, emailResult.ErrorMsg); + } + var email = emailResult.Value!; + + var getTicketsResult = await _shoppingCartService.GetTicketsFromCartAsync(email); + if (getTicketsResult.IsError) + { + return StatusCode(getTicketsResult.StatusCode, getTicketsResult.ErrorMsg); + } + + return Ok(getTicketsResult.Value); } [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] diff --git a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs index 83e2421..4df22fd 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs @@ -15,7 +15,7 @@ public ShoppingCartService(IShoppingCartRepository shoppingCartRepository) _shoppingCartRepository = shoppingCartRepository; } - public async Task AddNewTicketAsync(Guid ticketTypeId, string customerEmail, string? nameOnTicket, string? seats) + public async Task AddNewTicketToCartAsync(Guid ticketTypeId, string customerEmail, string? nameOnTicket, string? seats) { var getShoppingCartResult = await _shoppingCartRepository.GetShoppingCartByEmailAsync(customerEmail); @@ -43,7 +43,7 @@ public async Task AddNewTicketAsync(Guid ticketTypeId, string customerEm return Result.Success(); } - public async Task> GetTicketsAsync(string customerEmail) + public async Task> GetTicketsFromCartAsync(string customerEmail) { var getShoppingCartResult = await _shoppingCartRepository.GetShoppingCartByEmailAsync(customerEmail); @@ -58,7 +58,7 @@ public async Task> GetTicketsAsync(str return Result.Success(result); } - public Task RemoveTicketAsync() + public Task RemoveTicketFromCartAsync() { throw new NotImplementedException(); } From 95698fe539c4cbe76efbc82ee60de24787b7e326 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Fri, 16 May 2025 19:41:08 +0200 Subject: [PATCH 53/80] reworked ticket buying logic to make the pipeline from frontend more accessible --- .../Tickets/Services/TicketServiceTests.cs | 53 ++++++++++++------- TickAPI/TickAPI/Program.cs | 5 ++ .../Abstractions/IShoppingCartService.cs | 4 +- .../Controllers/ShoppingCartsController.cs | 11 ++-- .../DTOs/Request/AddNewTicketDto.cs | 3 +- .../Services/ShoppingCartService.cs | 40 +++++++++++--- .../Abstractions/ITicketTypeRepository.cs | 10 ++++ .../Repositories/TicketTypeRepository.cs | 32 +++++++++++ .../Tickets/Abstractions/ITicketService.cs | 2 + .../Tickets/Models/ShoppingCartNewTicket.cs | 3 +- .../Models/ShoppingCartResellTicket.cs | 1 - .../TickAPI/Tickets/Services/TicketService.cs | 32 ++++++++++- 12 files changed, 157 insertions(+), 39 deletions(-) create mode 100644 TickAPI/TickAPI/TicketTypes/Abstractions/ITicketTypeRepository.cs create mode 100644 TickAPI/TickAPI/TicketTypes/Repositories/TicketTypeRepository.cs diff --git a/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs b/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs index 331853f..aeaae9a 100644 --- a/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs @@ -14,6 +14,7 @@ using TickAPI.Tickets.DTOs.Response; using TickAPI.Tickets.Models; using TickAPI.Tickets.Services; +using TickAPI.TicketTypes.Abstractions; using TickAPI.TicketTypes.Models; namespace TickAPI.Tests.Tickets.Services; @@ -28,6 +29,7 @@ public void GetNumberOfAvailableTicketsByType_AmountsAreCorrect_ShouldReturnCorr var ticketList = new List(new Ticket[10]); var ticketRepositoryMock = new Mock(); + var ticketTypeRepositoryMock = new Mock(); var paginationServiceMock = new Mock(); var qrServiceMock = new Mock(); @@ -35,7 +37,7 @@ public void GetNumberOfAvailableTicketsByType_AmountsAreCorrect_ShouldReturnCorr .Setup(m => m.GetAllTicketsByTicketType(type)) .Returns(ticketList.AsQueryable()); - var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act var result = sut.GetNumberOfAvailableTicketsByType(type); @@ -53,6 +55,7 @@ public void GetNumberOfAvailableTicketsByType_WhenMoreTicketExistThanMaxCount_Sh var ticketList = new List(new Ticket[50]); var ticketRepositoryMock = new Mock(); + var ticketTypeRepositoryMock = new Mock(); var paginationServiceMock = new Mock(); var qrServiceMock = new Mock(); @@ -60,7 +63,7 @@ public void GetNumberOfAvailableTicketsByType_WhenMoreTicketExistThanMaxCount_Sh .Setup(m => m.GetAllTicketsByTicketType(type)) .Returns(ticketList.AsQueryable()); - var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act var result = sut.GetNumberOfAvailableTicketsByType(type); @@ -111,6 +114,8 @@ public async Task GetTicketsForResellAsync_WhenDataIsValid_ShouldReturnSuccess() ticketRepositoryMock.Setup(repo => repo.GetTicketsByEventId(eventId)) .Returns(allTickets); + var ticketTypeRepositoryMock = new Mock(); + var paginatedTickets = new PaginatedData( new List { ticket1, ticket2 }, page, @@ -156,7 +161,7 @@ public async Task GetTicketsForResellAsync_WhenDataIsValid_ShouldReturnSuccess() var qrServiceMock = new Mock(); - var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act var result = await sut.GetTicketsForResellAsync(eventId, page, pageSize); @@ -206,6 +211,8 @@ public async Task GetTicketsForResellAsync_WhenNoTicketsForResell_ShouldReturnEm ticketRepositoryMock.Setup(repo => repo.GetTicketsByEventId(eventId)) .Returns(tickets); + var ticketTypeRepositoryMock = new Mock(); + var paginatedData = new PaginatedData( new List(), page, @@ -234,7 +241,7 @@ public async Task GetTicketsForResellAsync_WhenNoTicketsForResell_ShouldReturnEm .Returns(mappedData); var qrServiceMock = new Mock(); - var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act var result = await sut.GetTicketsForResellAsync(eventId, page, pageSize); @@ -272,6 +279,8 @@ public async Task GetTicketsForResellAsync_WhenPaginationFails_ShouldPropagateEr var ticketRepositoryMock = new Mock(); ticketRepositoryMock.Setup(repo => repo.GetTicketsByEventId(eventId)) .Returns(tickets); + + var ticketTypeRepositoryMock = new Mock(); var paginationServiceMock = new Mock(); paginationServiceMock.Setup(p => p.PaginateAsync(It.IsAny>(), pageSize, page)) @@ -279,7 +288,7 @@ public async Task GetTicketsForResellAsync_WhenPaginationFails_ShouldPropagateEr var qrServiceMock = new Mock(); - var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act var result = await sut.GetTicketsForResellAsync(eventId, page, pageSize); @@ -304,6 +313,8 @@ public async Task GetTicketsForResellAsync_WhenNoTicketsForEvent_ShouldReturnEmp ticketRepositoryMock.Setup(repo => repo.GetTicketsByEventId(eventId)) .Returns(tickets); + var ticketTypeRepositoryMock = new Mock(); + var paginatedData = new PaginatedData( new List(), page, @@ -332,7 +343,7 @@ public async Task GetTicketsForResellAsync_WhenNoTicketsForEvent_ShouldReturnEmp .Returns(mappedData); var qrServiceMock = new Mock(); - var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act var result = await sut.GetTicketsForResellAsync(eventId, page, pageSize); @@ -380,9 +391,10 @@ public async Task GetTicketDetailsAsync_WhenTicketExistsForTheUser_ShouldReturnT } }, }; - string email = "123@123.com"; - string scanurl = "http://localhost"; - Mock ticketRepositoryMock = new Mock(); + const string email = "123@123.com"; + const string scanurl = "http://localhost"; + var ticketRepositoryMock = new Mock(); + var ticketTypeRepositoryMock = new Mock(); var paginationServiceMock = new Mock(); @@ -392,7 +404,7 @@ public async Task GetTicketDetailsAsync_WhenTicketExistsForTheUser_ShouldReturnT var qrServiceMock = new Mock(); qrServiceMock.Setup(m => m.GenerateQrCode(scanurl)).Returns([]); - var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act @@ -426,18 +438,19 @@ public async Task GetTicketDetailsAsync_WhenTicketDoesNotExistForTheUser_ShouldR // Arrange - Guid ticketId = Guid.NewGuid(); - string email = "123@123.com"; - string scanUrl = "http://localhost"; + var ticketId = Guid.NewGuid(); + const string email = "123@123.com"; + const string scanUrl = "http://localhost"; - Mock ticketRepositoryMock = new Mock(); + var ticketRepositoryMock = new Mock(); ticketRepositoryMock.Setup(m => m.GetTicketWithDetailsByIdAndEmailAsync(ticketId, email)). ReturnsAsync(Result.Failure(StatusCodes.Status404NotFound, "Ticket with this id doesn't exist " + "for this user")); + var ticketTypeRepositoryMock = new Mock(); var paginationServiceMock = new Mock(); var qrServiceMock = new Mock(); - var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act @@ -513,6 +526,7 @@ public async Task GetTicketsForCustomerAsync_WithValidInput_ReturnsSuccessResult var ticketRepositoryMock = new Mock(); ticketRepositoryMock.Setup(r => r.GetTicketsByCustomerEmail(email)).Returns(tickets.AsQueryable()); + var ticketTypeRepositoryMock = new Mock(); var paginationServiceMock = new Mock(); paginationServiceMock.Setup(p => p.PaginateAsync(tickets.AsQueryable(), pageSize, page)) @@ -523,7 +537,7 @@ public async Task GetTicketsForCustomerAsync_WithValidInput_ReturnsSuccessResult var qrServiceMock = new Mock(); - var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act var result = await sut.GetTicketsForCustomerAsync(email, page, pageSize); @@ -555,6 +569,8 @@ public async Task GetTicketsForCustomerAsync_WhenUserHasNoTickets_ReturnsEmptyPa var ticketRepositoryMock = new Mock(); ticketRepositoryMock.Setup(r => r.GetTicketsByCustomerEmail(email)).Returns(emptyTickets.AsQueryable()); + + var ticketTypeRepositoryMock = new Mock(); var paginationServiceMock = new Mock(); paginationServiceMock.Setup(p => p.PaginateAsync(emptyTickets.AsQueryable(), pageSize, page)).ReturnsAsync(paginatedResult); @@ -563,7 +579,7 @@ public async Task GetTicketsForCustomerAsync_WhenUserHasNoTickets_ReturnsEmptyPa var qrServiceMock = new Mock(); - var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act var result = await sut.GetTicketsForCustomerAsync(email, page, pageSize); @@ -580,9 +596,10 @@ public async Task ScanTicket_WhenScanningSuccesful_ShouldReturnSuccess() var guid = Guid.NewGuid(); var ticketRepositoryMock = new Mock(); ticketRepositoryMock.Setup(m => m.MarkTicketAsUsed(guid)).ReturnsAsync(Result.Success()); + var ticketTypeRepositoryMock = new Mock(); var paginationServiceMock = new Mock(); var qrServiceMock = new Mock(); - var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act var res = await sut.ScanTicket(guid); diff --git a/TickAPI/TickAPI/Program.cs b/TickAPI/TickAPI/Program.cs index e5cf94f..7d05eb3 100644 --- a/TickAPI/TickAPI/Program.cs +++ b/TickAPI/TickAPI/Program.cs @@ -47,6 +47,8 @@ using TickAPI.ShoppingCarts.Abstractions; using TickAPI.ShoppingCarts.Repositories; using TickAPI.ShoppingCarts.Services; +using TickAPI.TicketTypes.Abstractions; +using TickAPI.TicketTypes.Repositories; // Builder constants const string allowClientPolicyName = "AllowClient"; @@ -125,6 +127,9 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); +// Add ticket type services +builder.Services.AddScoped(); + // Add common services. builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs index 8bd2aee..5360d18 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs @@ -6,8 +6,8 @@ namespace TickAPI.ShoppingCarts.Abstractions; public interface IShoppingCartService { - public Task AddNewTicketToCartAsync(Guid ticketTypeId, string customerEmail, string? nameOnTicket, string? seats); + public Task AddNewTicketsToCartAsync(Guid ticketTypeId, uint amount, string customerEmail); public Task> GetTicketsFromCartAsync(string customerEmail); - public Task RemoveTicketFromCartAsync(); + public Task RemoveNewTicketsFromCartAsync(); public Task CheckoutAsync(); } \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs b/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs index aca5cc2..ad73641 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs @@ -23,7 +23,7 @@ public ShoppingCartsController(IShoppingCartService shoppingCartService, IClaims [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] [HttpPost] - public async Task AddTicket([FromBody] AddNewTicketDto addNewTicketDto) + public async Task AddTickets([FromBody] AddNewTicketDto addNewTicketDto) { var emailResult = _claimsService.GetEmailFromClaims(User.Claims); if (emailResult.IsError) @@ -32,8 +32,9 @@ public async Task AddTicket([FromBody] AddNewTicketDto addNewTicke } var email = emailResult.Value!; - var addTicketResult = await _shoppingCartService.AddNewTicketToCartAsync(addNewTicketDto.TicketTypeId, email, - addNewTicketDto.NameOnTicket, addNewTicketDto.Seats); + var addTicketResult = + await _shoppingCartService.AddNewTicketsToCartAsync(addNewTicketDto.TicketTypeId, addNewTicketDto.Amount, + email); if (addTicketResult.IsError) { return StatusCode(addTicketResult.StatusCode, addTicketResult.ErrorMsg); @@ -43,7 +44,7 @@ public async Task AddTicket([FromBody] AddNewTicketDto addNewTicke } [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] - [HttpGet("tickets")] + [HttpGet] public async Task> GetTickets() { var emailResult = _claimsService.GetEmailFromClaims(User.Claims); @@ -63,7 +64,7 @@ public async Task> GetTickets() } [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] - [HttpDelete("ticket")] + [HttpDelete] public async Task DeleteTicket() { throw new NotImplementedException(); diff --git a/TickAPI/TickAPI/ShoppingCarts/DTOs/Request/AddNewTicketDto.cs b/TickAPI/TickAPI/ShoppingCarts/DTOs/Request/AddNewTicketDto.cs index de86962..48c3d8e 100644 --- a/TickAPI/TickAPI/ShoppingCarts/DTOs/Request/AddNewTicketDto.cs +++ b/TickAPI/TickAPI/ShoppingCarts/DTOs/Request/AddNewTicketDto.cs @@ -2,6 +2,5 @@ public record AddNewTicketDto( Guid TicketTypeId, - string? NameOnTicket, - string? Seats + uint Amount ); \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs index 4df22fd..47a67cd 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs @@ -1,7 +1,9 @@ using TickAPI.Common.Results; using TickAPI.Common.Results.Generic; +using TickAPI.Events.Models; using TickAPI.ShoppingCarts.Abstractions; using TickAPI.ShoppingCarts.DTOs.Response; +using TickAPI.Tickets.Abstractions; using TickAPI.Tickets.Models; namespace TickAPI.ShoppingCarts.Services; @@ -9,14 +11,28 @@ namespace TickAPI.ShoppingCarts.Services; public class ShoppingCartService : IShoppingCartService { private readonly IShoppingCartRepository _shoppingCartRepository; + private readonly ITicketService _ticketService; - public ShoppingCartService(IShoppingCartRepository shoppingCartRepository) + public ShoppingCartService(IShoppingCartRepository shoppingCartRepository, ITicketService ticketService) { _shoppingCartRepository = shoppingCartRepository; + _ticketService = ticketService; } - public async Task AddNewTicketToCartAsync(Guid ticketTypeId, string customerEmail, string? nameOnTicket, string? seats) + public async Task AddNewTicketsToCartAsync(Guid ticketTypeId, uint amount, string customerEmail) { + var availabilityResult = _ticketService.CheckTicketAvailabilityByTypeId(ticketTypeId, amount); + + if (availabilityResult.IsError) + { + return Result.PropagateError(availabilityResult); + } + + if (!availabilityResult.Value) + { + return Result.Failure(StatusCodes.Status400BadRequest, $"not enough available tickets of type {ticketTypeId}"); + } + var getShoppingCartResult = await _shoppingCartRepository.GetShoppingCartByEmailAsync(customerEmail); if (getShoppingCartResult.IsError) @@ -26,12 +42,20 @@ public async Task AddNewTicketToCartAsync(Guid ticketTypeId, string cust var cart = getShoppingCartResult.Value!; - cart.NewTickets.Add(new ShoppingCartNewTicket() + var existingEntry = cart.NewTickets.FirstOrDefault(t => t.TicketTypeId == ticketTypeId); + + if (existingEntry != null) { - TicketTypeId = ticketTypeId, - NameOnTicket = nameOnTicket, - Seats = seats, - }); + existingEntry.Quantity += amount; + } + else + { + cart.NewTickets.Add(new ShoppingCartNewTicket + { + TicketTypeId = ticketTypeId, + Quantity = amount + }); + } var updateShoppingCartResult = await _shoppingCartRepository.UpdateShoppingCartAsync(customerEmail, cart); @@ -58,7 +82,7 @@ public async Task> GetTicketsFromCartA return Result.Success(result); } - public Task RemoveTicketFromCartAsync() + public Task RemoveNewTicketsFromCartAsync() { throw new NotImplementedException(); } diff --git a/TickAPI/TickAPI/TicketTypes/Abstractions/ITicketTypeRepository.cs b/TickAPI/TickAPI/TicketTypes/Abstractions/ITicketTypeRepository.cs new file mode 100644 index 0000000..403d5d2 --- /dev/null +++ b/TickAPI/TickAPI/TicketTypes/Abstractions/ITicketTypeRepository.cs @@ -0,0 +1,10 @@ +using TickAPI.Common.Results; +using TickAPI.Common.Results.Generic; +using TickAPI.TicketTypes.Models; + +namespace TickAPI.TicketTypes.Abstractions; + +public interface ITicketTypeRepository +{ + public Result GetTicketTypeById(Guid ticketTypeId); +} \ No newline at end of file diff --git a/TickAPI/TickAPI/TicketTypes/Repositories/TicketTypeRepository.cs b/TickAPI/TickAPI/TicketTypes/Repositories/TicketTypeRepository.cs new file mode 100644 index 0000000..2395a62 --- /dev/null +++ b/TickAPI/TickAPI/TicketTypes/Repositories/TicketTypeRepository.cs @@ -0,0 +1,32 @@ +using Microsoft.EntityFrameworkCore; +using TickAPI.Common.Results; +using TickAPI.Common.Results.Generic; +using TickAPI.Common.TickApiDbContext; +using TickAPI.TicketTypes.Abstractions; +using TickAPI.TicketTypes.Models; + +namespace TickAPI.TicketTypes.Repositories; + +public class TicketTypeRepository : ITicketTypeRepository +{ + private readonly TickApiDbContext _tickApiDbContext; + + public TicketTypeRepository(TickApiDbContext tickApiDbContext) + { + _tickApiDbContext = tickApiDbContext; + } + + public Result GetTicketTypeById(Guid ticketTypeId) + { + var ticketType = + _tickApiDbContext.TicketTypes + .FirstOrDefault(t => t.Id == ticketTypeId); + + if (ticketType == null) + { + return Result.Failure(StatusCodes.Status404NotFound,$"ticket type with id {ticketTypeId} not found"); + } + + return Result.Success(ticketType); + } +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs b/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs index c722a8c..d32de7a 100644 --- a/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs +++ b/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs @@ -10,6 +10,8 @@ namespace TickAPI.Tickets.Abstractions; public interface ITicketService { public Result GetNumberOfAvailableTicketsByType(TicketType ticketType); + public Result GetNumberOfAvailableTicketsByTypeId(Guid ticketTypeId); + public Result CheckTicketAvailabilityByTypeId(Guid ticketTypeId, uint amount); public Task>> GetTicketsForResellAsync(Guid eventId, int page, int pageSize); public Task>> GetTicketsForCustomerAsync(string email, int page, diff --git a/TickAPI/TickAPI/Tickets/Models/ShoppingCartNewTicket.cs b/TickAPI/TickAPI/Tickets/Models/ShoppingCartNewTicket.cs index 6521bc0..539c9ea 100644 --- a/TickAPI/TickAPI/Tickets/Models/ShoppingCartNewTicket.cs +++ b/TickAPI/TickAPI/Tickets/Models/ShoppingCartNewTicket.cs @@ -3,6 +3,5 @@ public class ShoppingCartNewTicket { public Guid TicketTypeId { get; set; } - public string? NameOnTicket { get; set; } - public string? Seats { get; set; } + public uint? Quantity { get; set; } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Models/ShoppingCartResellTicket.cs b/TickAPI/TickAPI/Tickets/Models/ShoppingCartResellTicket.cs index 81fc5a4..9ddb9ac 100644 --- a/TickAPI/TickAPI/Tickets/Models/ShoppingCartResellTicket.cs +++ b/TickAPI/TickAPI/Tickets/Models/ShoppingCartResellTicket.cs @@ -3,5 +3,4 @@ public class ShoppingCartResellTicket { public Guid TicketId { get; set; } - public string? NameOnTicket { get; set; } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Services/TicketService.cs b/TickAPI/TickAPI/Tickets/Services/TicketService.cs index 27b60fd..50b9e34 100644 --- a/TickAPI/TickAPI/Tickets/Services/TicketService.cs +++ b/TickAPI/TickAPI/Tickets/Services/TicketService.cs @@ -8,6 +8,7 @@ using TickAPI.Tickets.DTOs.Request; using TickAPI.Tickets.DTOs.Response; using TickAPI.Tickets.Filters; +using TickAPI.TicketTypes.Abstractions; using TickAPI.TicketTypes.Models; namespace TickAPI.Tickets.Services; @@ -15,11 +16,14 @@ namespace TickAPI.Tickets.Services; public class TicketService : ITicketService { private readonly ITicketRepository _ticketRepository; + private readonly ITicketTypeRepository _ticketTypeRepository; private readonly IPaginationService _paginationService; private readonly IQRCodeService _qrCodeService; - public TicketService(ITicketRepository ticketRepository, IPaginationService paginationService, IQRCodeService qrCodeService) + public TicketService(ITicketRepository ticketRepository, ITicketTypeRepository ticketTypeRepository, + IPaginationService paginationService, IQRCodeService qrCodeService) { _ticketRepository = ticketRepository; + _ticketTypeRepository = ticketTypeRepository; _paginationService = paginationService; _qrCodeService = qrCodeService; } @@ -40,6 +44,32 @@ public Result GetNumberOfAvailableTicketsByType(TicketType ticketType) return Result.Success((uint)availableCount); } + public Result GetNumberOfAvailableTicketsByTypeId(Guid ticketTypeId) + { + var ticketTypeResult = _ticketTypeRepository.GetTicketTypeById(ticketTypeId); + + if (ticketTypeResult.IsError) + { + return Result.PropagateError(ticketTypeResult); + } + + return GetNumberOfAvailableTicketsByType(ticketTypeResult.Value!); + } + + public Result CheckTicketAvailabilityByTypeId(Guid ticketTypeId, uint amount) + { + var numberOfTicketsResult = GetNumberOfAvailableTicketsByTypeId(ticketTypeId); + + if (numberOfTicketsResult.IsError) + { + return Result.PropagateError(numberOfTicketsResult); + } + + var availableAmount = numberOfTicketsResult.Value!; + + return availableAmount >= amount ? Result.Success(true) : Result.Success(false); + } + public async Task>> GetTicketsForResellAsync(Guid eventId, int page, int pageSize) { var eventTickets = _ticketRepository.GetTicketsByEventId(eventId); From 3edf2d6f6526b8f7460de382952b7dad83fc719b Mon Sep 17 00:00:00 2001 From: kubapoke Date: Fri, 16 May 2025 20:53:48 +0200 Subject: [PATCH 54/80] relegated some of the ticket adding logic to the repository --- .../Abstractions/IShoppingCartRepository.cs | 1 + .../Repositories/ShoppingCartRepository.cs | 37 +++++++++++++++++++ .../Services/ShoppingCartService.cs | 35 ++++-------------- 3 files changed, 46 insertions(+), 27 deletions(-) diff --git a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartRepository.cs b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartRepository.cs index af2d2d8..afd8f2b 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartRepository.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartRepository.cs @@ -8,4 +8,5 @@ public interface IShoppingCartRepository { public Task> GetShoppingCartByEmailAsync(string customerEmail); public Task UpdateShoppingCartAsync(string customerEmail, ShoppingCart shoppingCart); + public Task AddNewTicketToCartAsync(string customerEmail, Guid ticketTypeId, uint amount); } \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs b/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs index 6cda1c9..aa41651 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs @@ -3,6 +3,7 @@ using TickAPI.Common.Results.Generic; using TickAPI.ShoppingCarts.Abstractions; using TickAPI.ShoppingCarts.Models; +using TickAPI.Tickets.Models; namespace TickAPI.ShoppingCarts.Repositories; @@ -54,6 +55,42 @@ public async Task UpdateShoppingCartAsync(string customerEmail, Shopping return Result.Success(); } + public async Task AddNewTicketToCartAsync(string customerEmail, Guid ticketTypeId, uint amount) + { + var getShoppingCartResult = await GetShoppingCartByEmailAsync(customerEmail); + + if (getShoppingCartResult.IsError) + { + return Result.PropagateError(getShoppingCartResult); + } + + var cart = getShoppingCartResult.Value!; + + var existingEntry = cart.NewTickets.FirstOrDefault(t => t.TicketTypeId == ticketTypeId); + + if (existingEntry != null) + { + existingEntry.Quantity += amount; + } + else + { + cart.NewTickets.Add(new ShoppingCartNewTicket + { + TicketTypeId = ticketTypeId, + Quantity = amount + }); + } + + var updateShoppingCartResult = await UpdateShoppingCartAsync(customerEmail, cart); + + if (updateShoppingCartResult.IsError) + { + return Result.PropagateError(updateShoppingCartResult); + } + + return Result.Success(); + } + private static string GetCartKey(string customerEmail) { return $"cart:{customerEmail}"; diff --git a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs index 47a67cd..d069cb9 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs @@ -21,6 +21,11 @@ public ShoppingCartService(IShoppingCartRepository shoppingCartRepository, ITick public async Task AddNewTicketsToCartAsync(Guid ticketTypeId, uint amount, string customerEmail) { + if (amount <= 0) + { + return Result.Failure(StatusCodes.Status400BadRequest, "amount of bought tickets must be greater than 0"); + } + var availabilityResult = _ticketService.CheckTicketAvailabilityByTypeId(ticketTypeId, amount); if (availabilityResult.IsError) @@ -32,36 +37,12 @@ public async Task AddNewTicketsToCartAsync(Guid ticketTypeId, uint amoun { return Result.Failure(StatusCodes.Status400BadRequest, $"not enough available tickets of type {ticketTypeId}"); } - - var getShoppingCartResult = await _shoppingCartRepository.GetShoppingCartByEmailAsync(customerEmail); - - if (getShoppingCartResult.IsError) - { - return Result.PropagateError(getShoppingCartResult); - } - - var cart = getShoppingCartResult.Value!; - - var existingEntry = cart.NewTickets.FirstOrDefault(t => t.TicketTypeId == ticketTypeId); - - if (existingEntry != null) - { - existingEntry.Quantity += amount; - } - else - { - cart.NewTickets.Add(new ShoppingCartNewTicket - { - TicketTypeId = ticketTypeId, - Quantity = amount - }); - } - var updateShoppingCartResult = await _shoppingCartRepository.UpdateShoppingCartAsync(customerEmail, cart); + var addTicketToCartResult = await _shoppingCartRepository.AddNewTicketToCartAsync(customerEmail, ticketTypeId, amount); - if (updateShoppingCartResult.IsError) + if (addTicketToCartResult.IsError) { - return Result.PropagateError(updateShoppingCartResult); + return Result.PropagateError(addTicketToCartResult); } return Result.Success(); From 84dd97f923a77d5f8f1d8984cd4c686f2e4c74e5 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Fri, 16 May 2025 21:59:58 +0200 Subject: [PATCH 55/80] started updating some ticket retrieval logic --- ...gCartTicketsNewTicketDetailsResponseDto.cs | 9 ++++++ ...rtTicketsResellTicketDetailsResponseDto.cs | 9 ++++++ .../GetShoppingCartTicketsResponseDto.cs | 8 ++--- .../Mappers/ShoppingCartMapper.cs | 32 +++++++++++++++++++ .../Tickets/Models/ShoppingCartNewTicket.cs | 2 +- 5 files changed, 54 insertions(+), 6 deletions(-) create mode 100644 TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsNewTicketDetailsResponseDto.cs create mode 100644 TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsResellTicketDetailsResponseDto.cs create mode 100644 TickAPI/TickAPI/ShoppingCarts/Mappers/ShoppingCartMapper.cs diff --git a/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsNewTicketDetailsResponseDto.cs b/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsNewTicketDetailsResponseDto.cs new file mode 100644 index 0000000..408bb51 --- /dev/null +++ b/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsNewTicketDetailsResponseDto.cs @@ -0,0 +1,9 @@ +namespace TickAPI.ShoppingCarts.DTOs.Response; + +public record GetShoppingCartTicketsNewTicketDetailsResponseDto( + Guid TicketId, + string EventName, + string TicketType, + string OrganizerName, + uint Quantity +); \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsResellTicketDetailsResponseDto.cs b/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsResellTicketDetailsResponseDto.cs new file mode 100644 index 0000000..e014f96 --- /dev/null +++ b/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsResellTicketDetailsResponseDto.cs @@ -0,0 +1,9 @@ +namespace TickAPI.ShoppingCarts.DTOs.Response; + +public record GetShoppingCartTicketsResellTicketDetailsResponseDto( + Guid TicketId, + string EventName, + string TicketType, + string OrganizerName, + string OriginalOwnerEmail +); \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsResponseDto.cs b/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsResponseDto.cs index c4c3f96..3f553d6 100644 --- a/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsResponseDto.cs +++ b/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsResponseDto.cs @@ -1,8 +1,6 @@ -using TickAPI.Tickets.Models; - -namespace TickAPI.ShoppingCarts.DTOs.Response; +namespace TickAPI.ShoppingCarts.DTOs.Response; public record GetShoppingCartTicketsResponseDto( - List NewTickets, - List ResellTickets + List NewTickets, + List ResellTickets ); \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Mappers/ShoppingCartMapper.cs b/TickAPI/TickAPI/ShoppingCarts/Mappers/ShoppingCartMapper.cs new file mode 100644 index 0000000..95478d3 --- /dev/null +++ b/TickAPI/TickAPI/ShoppingCarts/Mappers/ShoppingCartMapper.cs @@ -0,0 +1,32 @@ +using TickAPI.ShoppingCarts.DTOs.Response; +using TickAPI.Tickets.Models; +using TickAPI.TicketTypes.Models; + +namespace TickAPI.ShoppingCarts.Mappers; + +public static class ShoppingCartMapper +{ + public static GetShoppingCartTicketsNewTicketDetailsResponseDto + MapTicketTypeToGetShoppingCartTicketsNewTicketDetailsResponseDto(TicketType type, uint quantity) + { + return new GetShoppingCartTicketsNewTicketDetailsResponseDto( + type.Id, + type.Event.Name, + type.Description, + type.Event.Organizer.DisplayName, + quantity + ); + } + + public static GetShoppingCartTicketsResellTicketDetailsResponseDto + MapTicketToGetShoppingCartTicketsResellTicketDetailsResponseDto(Ticket ticket) + { + return new GetShoppingCartTicketsResellTicketDetailsResponseDto( + ticket.Id, + ticket.Type.Event.Name, + ticket.Type.Description, + ticket.Type.Event.Organizer.DisplayName, + ticket.Owner.Email + ); + } +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Models/ShoppingCartNewTicket.cs b/TickAPI/TickAPI/Tickets/Models/ShoppingCartNewTicket.cs index 539c9ea..0ed97d9 100644 --- a/TickAPI/TickAPI/Tickets/Models/ShoppingCartNewTicket.cs +++ b/TickAPI/TickAPI/Tickets/Models/ShoppingCartNewTicket.cs @@ -3,5 +3,5 @@ public class ShoppingCartNewTicket { public Guid TicketTypeId { get; set; } - public uint? Quantity { get; set; } + public uint Quantity { get; set; } } \ No newline at end of file From ab228bedd5ea43e05eecc26d3a291544a943127e Mon Sep 17 00:00:00 2001 From: kubapoke Date: Tue, 20 May 2025 22:52:24 +0200 Subject: [PATCH 56/80] small aesthetic change in TicketRepository.cs --- .../TickAPI/Tickets/Repositories/TicketRepository.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs b/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs index e5abb5e..162d9f1 100644 --- a/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs +++ b/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs @@ -41,9 +41,13 @@ public IQueryable GetTicketsByCustomerEmail(string email) public async Task> GetTicketWithDetailsByIdAndEmailAsync(Guid id, string email) { - var ticket = await _tickApiDbContext.Tickets.Include(t => t.Type).Include(t => t.Type.Event) - .Include(t => t.Type.Event.Organizer).Include(t => t.Type.Event.Address) - .Where(t => (t.Id == id && t.Owner.Email == email)).FirstOrDefaultAsync(); + var ticket = await _tickApiDbContext.Tickets + .Include(t => t.Type) + .Include(t => t.Type.Event) + .Include(t => t.Type.Event.Organizer) + .Include(t => t.Type.Event.Address) + .Where(t => (t.Id == id && t.Owner.Email == email)) + .FirstOrDefaultAsync(); if (ticket == null) { return Result.Failure(StatusCodes.Status404NotFound, "Ticket with this id doesn't exist"); From 0f0e843b05211417ad5c6ac48e98cbc638f2d296 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Tue, 20 May 2025 23:31:15 +0200 Subject: [PATCH 57/80] updated cart retrieval logic to return more data --- ...gCartTicketsNewTicketDetailsResponseDto.cs | 2 +- .../Services/ShoppingCartService.cs | 24 +++++++++++++++++-- .../Abstractions/ITicketTypeRepository.cs | 2 +- .../Repositories/TicketTypeRepository.cs | 8 ++++--- .../Tickets/Abstractions/ITicketService.cs | 5 ++-- .../TickAPI/Tickets/Services/TicketService.cs | 20 ++++++++++++---- 6 files changed, 48 insertions(+), 13 deletions(-) diff --git a/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsNewTicketDetailsResponseDto.cs b/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsNewTicketDetailsResponseDto.cs index 408bb51..e0c4e9b 100644 --- a/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsNewTicketDetailsResponseDto.cs +++ b/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsNewTicketDetailsResponseDto.cs @@ -1,7 +1,7 @@ namespace TickAPI.ShoppingCarts.DTOs.Response; public record GetShoppingCartTicketsNewTicketDetailsResponseDto( - Guid TicketId, + Guid TicketTypeId, string EventName, string TicketType, string OrganizerName, diff --git a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs index d069cb9..f83cfe1 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs @@ -3,6 +3,7 @@ using TickAPI.Events.Models; using TickAPI.ShoppingCarts.Abstractions; using TickAPI.ShoppingCarts.DTOs.Response; +using TickAPI.ShoppingCarts.Mappers; using TickAPI.Tickets.Abstractions; using TickAPI.Tickets.Models; @@ -26,7 +27,7 @@ public async Task AddNewTicketsToCartAsync(Guid ticketTypeId, uint amoun return Result.Failure(StatusCodes.Status400BadRequest, "amount of bought tickets must be greater than 0"); } - var availabilityResult = _ticketService.CheckTicketAvailabilityByTypeId(ticketTypeId, amount); + var availabilityResult = await _ticketService.CheckTicketAvailabilityByTypeIdAsync(ticketTypeId, amount); if (availabilityResult.IsError) { @@ -58,7 +59,26 @@ public async Task> GetTicketsFromCartA } var cart = getShoppingCartResult.Value!; - var result = new GetShoppingCartTicketsResponseDto(cart.NewTickets, cart.ResellTickets); + + var newTickets = new List(); + + foreach (var ticket in cart.NewTickets) + { + var newTicketResult = await _ticketService.GetTicketTypeByIdAsync(ticket.TicketTypeId); + + if (newTicketResult.IsError) + { + return Result.PropagateError(newTicketResult); + } + + var newTicket = + ShoppingCartMapper.MapTicketTypeToGetShoppingCartTicketsNewTicketDetailsResponseDto( + newTicketResult.Value!, ticket.Quantity); + + newTickets.Add(newTicket); + } + + var result = new GetShoppingCartTicketsResponseDto(newTickets, []); return Result.Success(result); } diff --git a/TickAPI/TickAPI/TicketTypes/Abstractions/ITicketTypeRepository.cs b/TickAPI/TickAPI/TicketTypes/Abstractions/ITicketTypeRepository.cs index 403d5d2..f9fba2f 100644 --- a/TickAPI/TickAPI/TicketTypes/Abstractions/ITicketTypeRepository.cs +++ b/TickAPI/TickAPI/TicketTypes/Abstractions/ITicketTypeRepository.cs @@ -6,5 +6,5 @@ namespace TickAPI.TicketTypes.Abstractions; public interface ITicketTypeRepository { - public Result GetTicketTypeById(Guid ticketTypeId); + public Task> GetTicketTypeByIdAsync(Guid ticketTypeId); } \ No newline at end of file diff --git a/TickAPI/TickAPI/TicketTypes/Repositories/TicketTypeRepository.cs b/TickAPI/TickAPI/TicketTypes/Repositories/TicketTypeRepository.cs index 2395a62..929e471 100644 --- a/TickAPI/TickAPI/TicketTypes/Repositories/TicketTypeRepository.cs +++ b/TickAPI/TickAPI/TicketTypes/Repositories/TicketTypeRepository.cs @@ -16,11 +16,13 @@ public TicketTypeRepository(TickApiDbContext tickApiDbContext) _tickApiDbContext = tickApiDbContext; } - public Result GetTicketTypeById(Guid ticketTypeId) + public async Task> GetTicketTypeByIdAsync(Guid ticketTypeId) { - var ticketType = + var ticketType = await _tickApiDbContext.TicketTypes - .FirstOrDefault(t => t.Id == ticketTypeId); + .Include(t => t.Event) + .Include(t => t.Event.Organizer) + .FirstOrDefaultAsync(t => t.Id == ticketTypeId); if (ticketType == null) { diff --git a/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs b/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs index d32de7a..c396a0d 100644 --- a/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs +++ b/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs @@ -10,8 +10,8 @@ namespace TickAPI.Tickets.Abstractions; public interface ITicketService { public Result GetNumberOfAvailableTicketsByType(TicketType ticketType); - public Result GetNumberOfAvailableTicketsByTypeId(Guid ticketTypeId); - public Result CheckTicketAvailabilityByTypeId(Guid ticketTypeId, uint amount); + public Task> GetNumberOfAvailableTicketsByTypeIdAsync(Guid ticketTypeId); + public Task> CheckTicketAvailabilityByTypeIdAsync(Guid ticketTypeId, uint amount); public Task>> GetTicketsForResellAsync(Guid eventId, int page, int pageSize); public Task>> GetTicketsForCustomerAsync(string email, int page, @@ -19,4 +19,5 @@ public Task>> GetTicketsForCustome public Task ScanTicket(Guid ticketGuid); public Task> GetTicketDetailsAsync(Guid ticketGuid, string email, string scanUrl); + public Task> GetTicketTypeByIdAsync(Guid ticketTypeId); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Services/TicketService.cs b/TickAPI/TickAPI/Tickets/Services/TicketService.cs index 50b9e34..b24b704 100644 --- a/TickAPI/TickAPI/Tickets/Services/TicketService.cs +++ b/TickAPI/TickAPI/Tickets/Services/TicketService.cs @@ -44,9 +44,9 @@ public Result GetNumberOfAvailableTicketsByType(TicketType ticketType) return Result.Success((uint)availableCount); } - public Result GetNumberOfAvailableTicketsByTypeId(Guid ticketTypeId) + public async Task> GetNumberOfAvailableTicketsByTypeIdAsync(Guid ticketTypeId) { - var ticketTypeResult = _ticketTypeRepository.GetTicketTypeById(ticketTypeId); + var ticketTypeResult = await _ticketTypeRepository.GetTicketTypeByIdAsync(ticketTypeId); if (ticketTypeResult.IsError) { @@ -56,9 +56,9 @@ public Result GetNumberOfAvailableTicketsByTypeId(Guid ticketTypeId) return GetNumberOfAvailableTicketsByType(ticketTypeResult.Value!); } - public Result CheckTicketAvailabilityByTypeId(Guid ticketTypeId, uint amount) + public async Task> CheckTicketAvailabilityByTypeIdAsync(Guid ticketTypeId, uint amount) { - var numberOfTicketsResult = GetNumberOfAvailableTicketsByTypeId(ticketTypeId); + var numberOfTicketsResult = await GetNumberOfAvailableTicketsByTypeIdAsync(ticketTypeId); if (numberOfTicketsResult.IsError) { @@ -135,6 +135,18 @@ public async Task> GetTicketDetailsAsync(Gui return Result.Success(ticketDetails); } + public async Task> GetTicketTypeByIdAsync(Guid ticketTypeId) + { + var ticketTypeResult = await _ticketTypeRepository.GetTicketTypeByIdAsync(ticketTypeId); + + if (ticketTypeResult.IsError) + { + return Result.PropagateError(ticketTypeResult); + } + + return Result.Success(ticketTypeResult.Value!); + } + public async Task ScanTicket(Guid ticketGuid) { var res = await _ticketRepository.MarkTicketAsUsed(ticketGuid); From 5e3b95f56e44f16458992fd1e6efa4e364970ff0 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Tue, 20 May 2025 23:58:45 +0200 Subject: [PATCH 58/80] added functionality for removal of tickets from the shopping cart --- .../Abstractions/IShoppingCartRepository.cs | 3 +- .../Abstractions/IShoppingCartService.cs | 2 +- .../Controllers/ShoppingCartsController.cs | 23 ++++++-- .../DTOs/Request/RemoveNewTicketDto.cs | 6 ++ .../Repositories/ShoppingCartRepository.cs | 55 ++++++++++++++++++- .../Services/ShoppingCartService.cs | 22 ++++---- 6 files changed, 93 insertions(+), 18 deletions(-) create mode 100644 TickAPI/TickAPI/ShoppingCarts/DTOs/Request/RemoveNewTicketDto.cs diff --git a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartRepository.cs b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartRepository.cs index afd8f2b..9a1607d 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartRepository.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartRepository.cs @@ -8,5 +8,6 @@ public interface IShoppingCartRepository { public Task> GetShoppingCartByEmailAsync(string customerEmail); public Task UpdateShoppingCartAsync(string customerEmail, ShoppingCart shoppingCart); - public Task AddNewTicketToCartAsync(string customerEmail, Guid ticketTypeId, uint amount); + public Task AddNewTicketsToCartAsync(string customerEmail, Guid ticketTypeId, uint amount); + public Task RemoveNewTicketsFromCartAsync(string customerEmail, Guid ticketTypeId, uint amount); } \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs index 5360d18..aa48e4b 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs @@ -8,6 +8,6 @@ public interface IShoppingCartService { public Task AddNewTicketsToCartAsync(Guid ticketTypeId, uint amount, string customerEmail); public Task> GetTicketsFromCartAsync(string customerEmail); - public Task RemoveNewTicketsFromCartAsync(); + public Task RemoveNewTicketsFromCartAsync(Guid ticketTypeId, uint amount, string customerEmail); public Task CheckoutAsync(); } \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs b/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs index ad73641..fb9c6fe 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs @@ -23,7 +23,7 @@ public ShoppingCartsController(IShoppingCartService shoppingCartService, IClaims [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] [HttpPost] - public async Task AddTickets([FromBody] AddNewTicketDto addNewTicketDto) + public async Task AddTickets([FromBody] AddNewTicketDto addTicketDto) { var emailResult = _claimsService.GetEmailFromClaims(User.Claims); if (emailResult.IsError) @@ -33,7 +33,7 @@ public async Task AddTickets([FromBody] AddNewTicketDto addNewTick var email = emailResult.Value!; var addTicketResult = - await _shoppingCartService.AddNewTicketsToCartAsync(addNewTicketDto.TicketTypeId, addNewTicketDto.Amount, + await _shoppingCartService.AddNewTicketsToCartAsync(addTicketDto.TicketTypeId, addTicketDto.Amount, email); if (addTicketResult.IsError) { @@ -65,9 +65,24 @@ public async Task> GetTickets() [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] [HttpDelete] - public async Task DeleteTicket() + public async Task RemoveTickets([FromBody] RemoveNewTicketDto removeTicketDto) { - throw new NotImplementedException(); + var emailResult = _claimsService.GetEmailFromClaims(User.Claims); + if (emailResult.IsError) + { + return StatusCode(emailResult.StatusCode, emailResult.ErrorMsg); + } + var email = emailResult.Value!; + + var addTicketResult = + await _shoppingCartService.RemoveNewTicketsFromCartAsync(removeTicketDto.TicketTypeId, removeTicketDto.Amount, + email); + if (addTicketResult.IsError) + { + return StatusCode(addTicketResult.StatusCode, addTicketResult.ErrorMsg); + } + + return Ok(); } [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] diff --git a/TickAPI/TickAPI/ShoppingCarts/DTOs/Request/RemoveNewTicketDto.cs b/TickAPI/TickAPI/ShoppingCarts/DTOs/Request/RemoveNewTicketDto.cs new file mode 100644 index 0000000..012c376 --- /dev/null +++ b/TickAPI/TickAPI/ShoppingCarts/DTOs/Request/RemoveNewTicketDto.cs @@ -0,0 +1,6 @@ +namespace TickAPI.ShoppingCarts.DTOs.Request; + +public record RemoveNewTicketDto( + Guid TicketTypeId, + uint Amount +); \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs b/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs index aa41651..7e8f46e 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs @@ -44,7 +44,7 @@ public async Task UpdateShoppingCartAsync(string customerEmail, Shopping var res = await _redisService.SetObjectAsync(cartKey, shoppingCart, DefaultExpiry); if (!res) { - return Result.Failure(StatusCodes.Status500InternalServerError, "The shopping cart could not be updated."); + return Result.Failure(StatusCodes.Status500InternalServerError, "the shopping cart could not be updated"); } } catch (Exception e) @@ -55,8 +55,13 @@ public async Task UpdateShoppingCartAsync(string customerEmail, Shopping return Result.Success(); } - public async Task AddNewTicketToCartAsync(string customerEmail, Guid ticketTypeId, uint amount) + public async Task AddNewTicketsToCartAsync(string customerEmail, Guid ticketTypeId, uint amount) { + if (amount <= 0) + { + return Result.Failure(StatusCodes.Status400BadRequest, "amount of bought tickets must be greater than 0"); + } + var getShoppingCartResult = await GetShoppingCartByEmailAsync(customerEmail); if (getShoppingCartResult.IsError) @@ -91,6 +96,52 @@ public async Task AddNewTicketToCartAsync(string customerEmail, Guid tic return Result.Success(); } + public async Task RemoveNewTicketsFromCartAsync(string customerEmail, Guid ticketTypeId, uint amount) + { + if (amount <= 0) + { + return Result.Failure(StatusCodes.Status400BadRequest, "amount of removed tickets must be greater than 0"); + } + + var getShoppingCartResult = await GetShoppingCartByEmailAsync(customerEmail); + + if (getShoppingCartResult.IsError) + { + return Result.PropagateError(getShoppingCartResult); + } + + var cart = getShoppingCartResult.Value!; + + var existingEntry = cart.NewTickets.FirstOrDefault(t => t.TicketTypeId == ticketTypeId); + + if (existingEntry is null) + { + return Result.Failure(StatusCodes.Status404NotFound, "the shopping cart does not contain a ticket of this type"); + } + + if (existingEntry.Quantity < amount) + { + return Result.Failure(StatusCodes.Status400BadRequest, + $"the shopping cart does not contain {amount} tickets of this type"); + } + + existingEntry.Quantity -= amount; + + if (existingEntry.Quantity == 0) + { + cart.NewTickets.Remove(existingEntry); + } + + var updateShoppingCartResult = await UpdateShoppingCartAsync(customerEmail, cart); + + if (updateShoppingCartResult.IsError) + { + return Result.PropagateError(updateShoppingCartResult); + } + + return Result.Success(); + } + private static string GetCartKey(string customerEmail) { return $"cart:{customerEmail}"; diff --git a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs index f83cfe1..8e3f6f7 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs @@ -22,11 +22,6 @@ public ShoppingCartService(IShoppingCartRepository shoppingCartRepository, ITick public async Task AddNewTicketsToCartAsync(Guid ticketTypeId, uint amount, string customerEmail) { - if (amount <= 0) - { - return Result.Failure(StatusCodes.Status400BadRequest, "amount of bought tickets must be greater than 0"); - } - var availabilityResult = await _ticketService.CheckTicketAvailabilityByTypeIdAsync(ticketTypeId, amount); if (availabilityResult.IsError) @@ -39,11 +34,11 @@ public async Task AddNewTicketsToCartAsync(Guid ticketTypeId, uint amoun return Result.Failure(StatusCodes.Status400BadRequest, $"not enough available tickets of type {ticketTypeId}"); } - var addTicketToCartResult = await _shoppingCartRepository.AddNewTicketToCartAsync(customerEmail, ticketTypeId, amount); + var addTicketsToCartResult = await _shoppingCartRepository.AddNewTicketsToCartAsync(customerEmail, ticketTypeId, amount); - if (addTicketToCartResult.IsError) + if (addTicketsToCartResult.IsError) { - return Result.PropagateError(addTicketToCartResult); + return Result.PropagateError(addTicketsToCartResult); } return Result.Success(); @@ -83,9 +78,16 @@ public async Task> GetTicketsFromCartA return Result.Success(result); } - public Task RemoveNewTicketsFromCartAsync() + public async Task RemoveNewTicketsFromCartAsync(Guid ticketTypeId, uint amount, string customerEmail) { - throw new NotImplementedException(); + var removeTicketsFromCartResult = await _shoppingCartRepository.RemoveNewTicketsFromCartAsync(customerEmail, ticketTypeId, amount); + + if (removeTicketsFromCartResult.IsError) + { + return Result.PropagateError(removeTicketsFromCartResult); + } + + return Result.Success(); } public Task CheckoutAsync() From e877eae225e0dd63a0ab13244a696d3fa90883c7 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Thu, 22 May 2025 01:13:44 +0200 Subject: [PATCH 59/80] implemented getting cart due amount and part of the checkout process --- .../Abstractions/IShoppingCartService.cs | 8 +- .../Controllers/ShoppingCartsController.cs | 21 +++++ ...gCartTicketsNewTicketDetailsResponseDto.cs | 3 +- ...rtTicketsResellTicketDetailsResponseDto.cs | 3 +- .../Mappers/ShoppingCartMapper.cs | 6 +- .../Services/ShoppingCartService.cs | 77 ++++++++++++++++++- 6 files changed, 108 insertions(+), 10 deletions(-) diff --git a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs index aa48e4b..12f914c 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs @@ -1,4 +1,5 @@ -using TickAPI.Common.Results; +using TickAPI.Common.Payment.Models; +using TickAPI.Common.Results; using TickAPI.Common.Results.Generic; using TickAPI.ShoppingCarts.DTOs.Response; @@ -9,5 +10,8 @@ public interface IShoppingCartService public Task AddNewTicketsToCartAsync(Guid ticketTypeId, uint amount, string customerEmail); public Task> GetTicketsFromCartAsync(string customerEmail); public Task RemoveNewTicketsFromCartAsync(Guid ticketTypeId, uint amount, string customerEmail); - public Task CheckoutAsync(); + public Task> GetDueAmountAsync(string customerEmail, string currency); + public Task> CheckoutAsync(string customerEmail, decimal amount, string currency, + string cardNumber, + string cardExpiry, string cvv); } \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs b/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs index fb9c6fe..c477810 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs @@ -84,6 +84,27 @@ await _shoppingCartService.RemoveNewTicketsFromCartAsync(removeTicketDto.TicketT return Ok(); } + + [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] + [HttpGet("due/{currency}")] + public async Task> GetDueAmount([FromRoute] string currency) + { + var emailResult = _claimsService.GetEmailFromClaims(User.Claims); + if (emailResult.IsError) + { + return StatusCode(emailResult.StatusCode, emailResult.ErrorMsg); + } + var email = emailResult.Value!; + + var dueAmountResult = await _shoppingCartService.GetDueAmountAsync(email, currency); + + if (dueAmountResult.IsError) + { + return StatusCode(dueAmountResult.StatusCode, dueAmountResult.ErrorMsg); + } + + return Ok(dueAmountResult.Value); + } [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] [HttpPost("checkout")] diff --git a/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsNewTicketDetailsResponseDto.cs b/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsNewTicketDetailsResponseDto.cs index e0c4e9b..f85e035 100644 --- a/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsNewTicketDetailsResponseDto.cs +++ b/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsNewTicketDetailsResponseDto.cs @@ -5,5 +5,6 @@ public record GetShoppingCartTicketsNewTicketDetailsResponseDto( string EventName, string TicketType, string OrganizerName, - uint Quantity + uint Quantity, + decimal UnitPrice ); \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsResellTicketDetailsResponseDto.cs b/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsResellTicketDetailsResponseDto.cs index e014f96..f2aa4d0 100644 --- a/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsResellTicketDetailsResponseDto.cs +++ b/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsResellTicketDetailsResponseDto.cs @@ -5,5 +5,6 @@ public record GetShoppingCartTicketsResellTicketDetailsResponseDto( string EventName, string TicketType, string OrganizerName, - string OriginalOwnerEmail + string OriginalOwnerEmail, + decimal Price ); \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Mappers/ShoppingCartMapper.cs b/TickAPI/TickAPI/ShoppingCarts/Mappers/ShoppingCartMapper.cs index 95478d3..ae3d653 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Mappers/ShoppingCartMapper.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Mappers/ShoppingCartMapper.cs @@ -14,7 +14,8 @@ public static GetShoppingCartTicketsNewTicketDetailsResponseDto type.Event.Name, type.Description, type.Event.Organizer.DisplayName, - quantity + quantity, + type.Price ); } @@ -26,7 +27,8 @@ public static GetShoppingCartTicketsResellTicketDetailsResponseDto ticket.Type.Event.Name, ticket.Type.Description, ticket.Type.Event.Organizer.DisplayName, - ticket.Owner.Email + ticket.Owner.Email, + ticket.Type.Price ); } } \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs index 8e3f6f7..a53a9d1 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs @@ -1,4 +1,6 @@ -using TickAPI.Common.Results; +using TickAPI.Common.Payment.Abstractions; +using TickAPI.Common.Payment.Models; +using TickAPI.Common.Results; using TickAPI.Common.Results.Generic; using TickAPI.Events.Models; using TickAPI.ShoppingCarts.Abstractions; @@ -13,11 +15,14 @@ public class ShoppingCartService : IShoppingCartService { private readonly IShoppingCartRepository _shoppingCartRepository; private readonly ITicketService _ticketService; + private readonly IPaymentGatewayService _paymentGatewayService; - public ShoppingCartService(IShoppingCartRepository shoppingCartRepository, ITicketService ticketService) + public ShoppingCartService(IShoppingCartRepository shoppingCartRepository, ITicketService ticketService, + IPaymentGatewayService paymentGatewayService) { _shoppingCartRepository = shoppingCartRepository; _ticketService = ticketService; + _paymentGatewayService = paymentGatewayService; } public async Task AddNewTicketsToCartAsync(Guid ticketTypeId, uint amount, string customerEmail) @@ -73,6 +78,8 @@ public async Task> GetTicketsFromCartA newTickets.Add(newTicket); } + // TODO: Add resell ticket parsing + var result = new GetShoppingCartTicketsResponseDto(newTickets, []); return Result.Success(result); @@ -90,8 +97,70 @@ public async Task RemoveNewTicketsFromCartAsync(Guid ticketTypeId, uint return Result.Success(); } - public Task CheckoutAsync() + public async Task> GetDueAmountAsync(string customerEmail, string currency) { - throw new NotImplementedException(); + var getShoppingCartResult = await _shoppingCartRepository.GetShoppingCartByEmailAsync(customerEmail); + + if (getShoppingCartResult.IsError) + { + return Result.PropagateError(getShoppingCartResult); + } + + var cart = getShoppingCartResult.Value!; + + decimal total = 0; + + foreach (var newTicket in cart.NewTickets) + { + var ticketTypeResult = await _ticketService.GetTicketTypeByIdAsync(newTicket.TicketTypeId); + + if (ticketTypeResult.IsError) + { + return Result.PropagateError(ticketTypeResult); + } + + var ticketType = ticketTypeResult.Value!; + + if (ticketType.Currency == currency) + { + total += newTicket.Quantity * ticketType.Price; + } + } + + // TODO: Add resell tickets to the calculations + + return Result.Success(total); + } + + public async Task> CheckoutAsync(string customerEmail, decimal amount, string currency, + string cardNumber, string cardExpiry, string cvv) + { + var dueAmountResult = await GetDueAmountAsync(customerEmail, currency); + + if (dueAmountResult.IsError) + { + return Result.PropagateError(dueAmountResult); + } + + var dueAmount = dueAmountResult.Value; + + if (dueAmount != amount) + { + return Result.Failure(StatusCodes.Status400BadRequest, + $"the given amount {amount} {currency} is different than the expected amount of {dueAmount} {currency}"); + } + + var paymentResult = + await _paymentGatewayService.ProcessPayment(new PaymentRequestPG(amount, currency, cardNumber, cardExpiry, + cvv, false)); + + if (paymentResult.IsError) + { + return Result.PropagateError(paymentResult); + } + + var payment = paymentResult.Value!; + + return Result.Success(payment); } } \ No newline at end of file From 68d9662eee0e21070df487eb283f4c57b0b3f8d2 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Thu, 22 May 2025 01:23:32 +0200 Subject: [PATCH 60/80] added payment endpoint --- .../Controllers/ShoppingCartsController.cs | 21 +++++++++++++++++-- .../ShoppingCarts/DTOs/Request/CheckoutDto.cs | 9 ++++++++ .../DTOs/Response/CheckoutResponseDto.cs | 6 ++++++ 3 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 TickAPI/TickAPI/ShoppingCarts/DTOs/Request/CheckoutDto.cs create mode 100644 TickAPI/TickAPI/ShoppingCarts/DTOs/Response/CheckoutResponseDto.cs diff --git a/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs b/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs index c477810..a869b6a 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs @@ -108,8 +108,25 @@ public async Task> GetDueAmount([FromRoute] string currenc [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] [HttpPost("checkout")] - public async Task Checkout() + public async Task> Checkout([FromBody] CheckoutDto checkoutDto) { - throw new NotImplementedException(); + var emailResult = _claimsService.GetEmailFromClaims(User.Claims); + if (emailResult.IsError) + { + return StatusCode(emailResult.StatusCode, emailResult.ErrorMsg); + } + var email = emailResult.Value!; + + var checkoutResult = await _shoppingCartService.CheckoutAsync(email, checkoutDto.Amount, checkoutDto.Currency, + checkoutDto.CardNumber, checkoutDto.CardExpiry, checkoutDto.Cvv); + + if (checkoutResult.IsError) + { + return StatusCode(checkoutResult.StatusCode, checkoutResult.ErrorMsg); + } + + var checkout = checkoutResult.Value!; + + return Ok(new CheckoutResponseDto(checkout.TransactionId, checkout.Status)); } } \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/DTOs/Request/CheckoutDto.cs b/TickAPI/TickAPI/ShoppingCarts/DTOs/Request/CheckoutDto.cs new file mode 100644 index 0000000..aae0e68 --- /dev/null +++ b/TickAPI/TickAPI/ShoppingCarts/DTOs/Request/CheckoutDto.cs @@ -0,0 +1,9 @@ +namespace TickAPI.ShoppingCarts.DTOs.Request; + +public record CheckoutDto( + decimal Amount, + string Currency, + string CardNumber, + string CardExpiry, + string Cvv +); \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/CheckoutResponseDto.cs b/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/CheckoutResponseDto.cs new file mode 100644 index 0000000..6a4b4f2 --- /dev/null +++ b/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/CheckoutResponseDto.cs @@ -0,0 +1,6 @@ +namespace TickAPI.ShoppingCarts.DTOs.Response; + +public record CheckoutResponseDto( + string TransactionId, + string Status +); \ No newline at end of file From 05da8e5eb3d1e3ab0875351290f4103bfef545a8 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Thu, 22 May 2025 01:46:06 +0200 Subject: [PATCH 61/80] changed due amount calculations --- .../Abstractions/IShoppingCartService.cs | 2 +- .../Controllers/ShoppingCartsController.cs | 6 +-- .../Services/ShoppingCartService.cs | 45 ++++++++++++++----- 3 files changed, 38 insertions(+), 15 deletions(-) diff --git a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs index 12f914c..47add30 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs @@ -10,7 +10,7 @@ public interface IShoppingCartService public Task AddNewTicketsToCartAsync(Guid ticketTypeId, uint amount, string customerEmail); public Task> GetTicketsFromCartAsync(string customerEmail); public Task RemoveNewTicketsFromCartAsync(Guid ticketTypeId, uint amount, string customerEmail); - public Task> GetDueAmountAsync(string customerEmail, string currency); + public Task>> GetDueAmountAsync(string customerEmail); public Task> CheckoutAsync(string customerEmail, decimal amount, string currency, string cardNumber, string cardExpiry, string cvv); diff --git a/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs b/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs index a869b6a..1780d8d 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs @@ -86,8 +86,8 @@ await _shoppingCartService.RemoveNewTicketsFromCartAsync(removeTicketDto.TicketT } [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] - [HttpGet("due/{currency}")] - public async Task> GetDueAmount([FromRoute] string currency) + [HttpGet("due")] + public async Task>> GetDueAmount() { var emailResult = _claimsService.GetEmailFromClaims(User.Claims); if (emailResult.IsError) @@ -96,7 +96,7 @@ public async Task> GetDueAmount([FromRoute] string currenc } var email = emailResult.Value!; - var dueAmountResult = await _shoppingCartService.GetDueAmountAsync(email, currency); + var dueAmountResult = await _shoppingCartService.GetDueAmountAsync(email); if (dueAmountResult.IsError) { diff --git a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs index a53a9d1..c671d98 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs @@ -1,4 +1,5 @@ -using TickAPI.Common.Payment.Abstractions; +using Google.Apis.Auth.OAuth2.Web; +using TickAPI.Common.Payment.Abstractions; using TickAPI.Common.Payment.Models; using TickAPI.Common.Results; using TickAPI.Common.Results.Generic; @@ -97,18 +98,18 @@ public async Task RemoveNewTicketsFromCartAsync(Guid ticketTypeId, uint return Result.Success(); } - public async Task> GetDueAmountAsync(string customerEmail, string currency) + public async Task>> GetDueAmountAsync(string customerEmail) { var getShoppingCartResult = await _shoppingCartRepository.GetShoppingCartByEmailAsync(customerEmail); if (getShoppingCartResult.IsError) { - return Result.PropagateError(getShoppingCartResult); + return Result>.PropagateError(getShoppingCartResult); } var cart = getShoppingCartResult.Value!; - decimal total = 0; + Dictionary dueAmount = new Dictionary(); foreach (var newTicket in cart.NewTickets) { @@ -116,34 +117,44 @@ public async Task> GetDueAmountAsync(string customerEmail, strin if (ticketTypeResult.IsError) { - return Result.PropagateError(ticketTypeResult); + return Result>.PropagateError(ticketTypeResult); } var ticketType = ticketTypeResult.Value!; - - if (ticketType.Currency == currency) + + if(dueAmount.ContainsKey(ticketType.Currency)) + { + dueAmount[ticketType.Currency] += newTicket.Quantity * ticketType.Price; + } + else { - total += newTicket.Quantity * ticketType.Price; + dueAmount.Add(ticketType.Currency, newTicket.Quantity * ticketType.Price); } } // TODO: Add resell tickets to the calculations - return Result.Success(total); + return Result>.Success(dueAmount); } public async Task> CheckoutAsync(string customerEmail, decimal amount, string currency, string cardNumber, string cardExpiry, string cvv) { - var dueAmountResult = await GetDueAmountAsync(customerEmail, currency); + var dueAmountResult = await GetDueAmountAsync(customerEmail); if (dueAmountResult.IsError) { return Result.PropagateError(dueAmountResult); } - var dueAmount = dueAmountResult.Value; + var currencyExists = dueAmountResult.Value!.TryGetValue(currency, out var dueAmount); + if (!currencyExists) + { + return Result.Failure(StatusCodes.Status400BadRequest, + $"no tickets paid in {currency} found in cart"); + } + if (dueAmount != amount) { return Result.Failure(StatusCodes.Status400BadRequest, @@ -158,9 +169,21 @@ await _paymentGatewayService.ProcessPayment(new PaymentRequestPG(amount, currenc { return Result.PropagateError(paymentResult); } + + var generateTicketsResult = await GenerateBoughtTicketsAsync(customerEmail, currency); + if (generateTicketsResult.IsError) + { + return Result.PropagateError(generateTicketsResult); + } + var payment = paymentResult.Value!; return Result.Success(payment); } + + private static async Task GenerateBoughtTicketsAsync(string customerEmail, string currency) + { + throw new NotImplementedException(); + } } \ No newline at end of file From 2338c8e760f80c95a76ab5de4b77ddc2a4747295 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Thu, 22 May 2025 18:26:34 +0200 Subject: [PATCH 62/80] implemented adding created tickets to database --- .../Abstractions/IShoppingCartService.cs | 3 +- .../Services/ShoppingCartService.cs | 56 +++++++++++++++++-- .../Tickets/Abstractions/ITicketRepository.cs | 2 + .../Tickets/Abstractions/ITicketService.cs | 4 ++ .../Tickets/Repositories/TicketRepository.cs | 17 ++++++ .../TickAPI/Tickets/Services/TicketService.cs | 26 ++++++++- 6 files changed, 99 insertions(+), 9 deletions(-) diff --git a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs index 47add30..787e62e 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartService.cs @@ -12,6 +12,5 @@ public interface IShoppingCartService public Task RemoveNewTicketsFromCartAsync(Guid ticketTypeId, uint amount, string customerEmail); public Task>> GetDueAmountAsync(string customerEmail); public Task> CheckoutAsync(string customerEmail, decimal amount, string currency, - string cardNumber, - string cardExpiry, string cvv); + string cardNumber, string cardExpiry, string cvv); } \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs index c671d98..e459c54 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs @@ -3,25 +3,29 @@ using TickAPI.Common.Payment.Models; using TickAPI.Common.Results; using TickAPI.Common.Results.Generic; +using TickAPI.Customers.Abstractions; using TickAPI.Events.Models; using TickAPI.ShoppingCarts.Abstractions; using TickAPI.ShoppingCarts.DTOs.Response; using TickAPI.ShoppingCarts.Mappers; using TickAPI.Tickets.Abstractions; using TickAPI.Tickets.Models; +using TickAPI.TicketTypes.Abstractions; namespace TickAPI.ShoppingCarts.Services; public class ShoppingCartService : IShoppingCartService { private readonly IShoppingCartRepository _shoppingCartRepository; + private readonly ICustomerRepository _customerRepository; private readonly ITicketService _ticketService; private readonly IPaymentGatewayService _paymentGatewayService; - public ShoppingCartService(IShoppingCartRepository shoppingCartRepository, ITicketService ticketService, - IPaymentGatewayService paymentGatewayService) + public ShoppingCartService(IShoppingCartRepository shoppingCartRepository, ICustomerRepository customerRepository, + ITicketService ticketService, IPaymentGatewayService paymentGatewayService) { _shoppingCartRepository = shoppingCartRepository; + _customerRepository = customerRepository; _ticketService = ticketService; _paymentGatewayService = paymentGatewayService; } @@ -182,8 +186,52 @@ await _paymentGatewayService.ProcessPayment(new PaymentRequestPG(amount, currenc return Result.Success(payment); } - private static async Task GenerateBoughtTicketsAsync(string customerEmail, string currency) + private async Task GenerateBoughtTicketsAsync(string customerEmail, string currency) { - throw new NotImplementedException(); + var getShoppingCartResult = await _shoppingCartRepository.GetShoppingCartByEmailAsync(customerEmail); + + if (getShoppingCartResult.IsError) + { + return Result.PropagateError(getShoppingCartResult); + } + + var cart = getShoppingCartResult.Value!; + + var getCustomerResult = await _customerRepository.GetCustomerByEmailAsync(customerEmail); + + if (getCustomerResult.IsError) + { + return Result.PropagateError(getCustomerResult); + } + + var owner = getCustomerResult.Value!; + + foreach (var ticket in cart.NewTickets) + { + var ticketTypeResult = await _ticketService.GetTicketTypeByIdAsync(ticket.TicketTypeId); + + if (ticketTypeResult.IsError) + { + return Result.PropagateError(ticketTypeResult); + } + + var type = ticketTypeResult.Value!; + + if (type.Currency == currency) + { + for (var i = 0; i < ticket.Quantity; i++) + { + // TODO: add seats/name on ticket setting + var createTicketResult = await _ticketService.CreateTicketAsync(type, owner); + + if (createTicketResult.IsError) + { + return Result.PropagateError(createTicketResult); + } + } + } + } + + return Result.Success(); } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs b/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs index 6290950..877b9cd 100644 --- a/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs +++ b/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs @@ -1,5 +1,6 @@ using TickAPI.Common.Results; using TickAPI.Common.Results.Generic; +using TickAPI.Customers.Models; using TickAPI.Tickets.Models; using TickAPI.TicketTypes.Models; @@ -12,4 +13,5 @@ public interface ITicketRepository public IQueryable GetTicketsByEventId(Guid eventId); public IQueryable GetTicketsByCustomerEmail(string email); public Task MarkTicketAsUsed(Guid id); + public Task AddTicketAsync(Ticket ticket); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs b/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs index c396a0d..8b75bd3 100644 --- a/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs +++ b/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs @@ -1,6 +1,7 @@ using TickAPI.Common.Pagination.Responses; using TickAPI.Common.Results; using TickAPI.Common.Results.Generic; +using TickAPI.Customers.Models; using TickAPI.Tickets.DTOs.Request; using TickAPI.Tickets.DTOs.Response; using TickAPI.TicketTypes.Models; @@ -20,4 +21,7 @@ public Task>> GetTicketsForCustome public Task> GetTicketDetailsAsync(Guid ticketGuid, string email, string scanUrl); public Task> GetTicketTypeByIdAsync(Guid ticketTypeId); + + public Task CreateTicketAsync(TicketType type, Customer owner, string? nameOnTicket = null, + string? seats = null); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs b/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs index 162d9f1..57774e6 100644 --- a/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs +++ b/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs @@ -2,6 +2,7 @@ using TickAPI.Common.Results; using TickAPI.Common.Results.Generic; using TickAPI.Common.TickApiDbContext; +using TickAPI.Customers.Models; using TickAPI.Tickets.Abstractions; using TickAPI.Tickets.Models; using TickAPI.TicketTypes.Models; @@ -70,4 +71,20 @@ public async Task MarkTicketAsUsed(Guid id) await _tickApiDbContext.SaveChangesAsync(); return Result.Success(); } + + public async Task AddTicketAsync(Ticket ticket) + { + var maxCount = ticket.Type.MaxCount; + + if (maxCount <= _tickApiDbContext.Tickets.Count(t => t.Type.Id == ticket.Type.Id)) + { + return Result.Failure(StatusCodes.Status400BadRequest, + "The ticket you are trying to buy has already reached its max count"); + } + + _tickApiDbContext.Tickets.Add(ticket); + await _tickApiDbContext.SaveChangesAsync(); + + return Result.Success(); + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Services/TicketService.cs b/TickAPI/TickAPI/Tickets/Services/TicketService.cs index b24b704..e141438 100644 --- a/TickAPI/TickAPI/Tickets/Services/TicketService.cs +++ b/TickAPI/TickAPI/Tickets/Services/TicketService.cs @@ -4,10 +4,13 @@ using TickAPI.Common.QR.Abstractions; using TickAPI.Common.Results; using TickAPI.Common.Results.Generic; +using TickAPI.Customers.Abstractions; +using TickAPI.Customers.Models; using TickAPI.Tickets.Abstractions; using TickAPI.Tickets.DTOs.Request; using TickAPI.Tickets.DTOs.Response; using TickAPI.Tickets.Filters; +using TickAPI.Tickets.Models; using TickAPI.TicketTypes.Abstractions; using TickAPI.TicketTypes.Models; @@ -19,7 +22,8 @@ public class TicketService : ITicketService private readonly ITicketTypeRepository _ticketTypeRepository; private readonly IPaginationService _paginationService; private readonly IQRCodeService _qrCodeService; - public TicketService(ITicketRepository ticketRepository, ITicketTypeRepository ticketTypeRepository, + + public TicketService(ITicketRepository ticketRepository, ITicketTypeRepository ticketTypeRepository, IPaginationService paginationService, IQRCodeService qrCodeService) { _ticketRepository = ticketRepository; @@ -147,11 +151,27 @@ public async Task> GetTicketTypeByIdAsync(Guid ticketTypeId) return Result.Success(ticketTypeResult.Value!); } + public async Task CreateTicketAsync(TicketType type, Customer owner, string? nameOnTicket = null, + string? seats = null) + { + var ticket = new Ticket + { + Type = type, + Owner = owner, + NameOnTicket = nameOnTicket, + Seats = seats, + ForResell = false, + Used = false, + }; + + var addTicketResult = await _ticketRepository.AddTicketAsync(ticket); + + return addTicketResult; + } + public async Task ScanTicket(Guid ticketGuid) { var res = await _ticketRepository.MarkTicketAsUsed(ticketGuid); return res; } - - } \ No newline at end of file From b0703debf6489fc95226fdcd2f062cbde47bf79f Mon Sep 17 00:00:00 2001 From: kubapoke Date: Thu, 22 May 2025 18:40:22 +0200 Subject: [PATCH 63/80] tickets removed from cart after purchase --- .../ShoppingCarts/Services/ShoppingCartService.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs index e459c54..b4032e5 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Services/ShoppingCartService.cs @@ -175,6 +175,7 @@ await _paymentGatewayService.ProcessPayment(new PaymentRequestPG(amount, currenc } var generateTicketsResult = await GenerateBoughtTicketsAsync(customerEmail, currency); + // TODO: Add passing ownership of resell tickets if (generateTicketsResult.IsError) { @@ -205,6 +206,7 @@ private async Task GenerateBoughtTicketsAsync(string customerEmail, stri } var owner = getCustomerResult.Value!; + var removals = new List<(Guid id, uint amount)>(); foreach (var ticket in cart.NewTickets) { @@ -219,6 +221,8 @@ private async Task GenerateBoughtTicketsAsync(string customerEmail, stri if (type.Currency == currency) { + removals.Add((ticket.TicketTypeId, ticket.Quantity)); + for (var i = 0; i < ticket.Quantity; i++) { // TODO: add seats/name on ticket setting @@ -232,6 +236,16 @@ private async Task GenerateBoughtTicketsAsync(string customerEmail, stri } } + foreach (var (id, amount) in removals) + { + var removalResult = await RemoveNewTicketsFromCartAsync(id, amount, customerEmail); + + if (removalResult.IsError) + { + return Result.PropagateError(removalResult); + } + } + return Result.Success(); } } \ No newline at end of file From fe4cf05d84dd5b7bb5afd9366e9bab895c8b0a86 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Thu, 22 May 2025 19:05:49 +0200 Subject: [PATCH 64/80] added methods for manipulating ticket type counters --- .../Abstractions/IShoppingCartRepository.cs | 5 + .../Repositories/ShoppingCartRepository.cs | 100 ++++++++++++++++++ 2 files changed, 105 insertions(+) diff --git a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartRepository.cs b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartRepository.cs index 9a1607d..d8be591 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartRepository.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Abstractions/IShoppingCartRepository.cs @@ -10,4 +10,9 @@ public interface IShoppingCartRepository public Task UpdateShoppingCartAsync(string customerEmail, ShoppingCart shoppingCart); public Task AddNewTicketsToCartAsync(string customerEmail, Guid ticketTypeId, uint amount); public Task RemoveNewTicketsFromCartAsync(string customerEmail, Guid ticketTypeId, uint amount); + public Task> GetAmountOfTicketTypeAsync(Guid ticketTypeId); + public Task SetAmountOfTicketTypeAsync(Guid ticketTypeId, long amount); + public Task> IncrementAmountOfTicketTypeAsync(Guid ticketTypeId, long amount); + public Task> DecrementAmountOfTicketTypeAsync(Guid ticketTypeId, long amount); + public Task RemoveAmountOfTicketTypeAsync(Guid ticketTypeId); } \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs b/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs index 7e8f46e..2b15778 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs @@ -142,8 +142,108 @@ public async Task RemoveNewTicketsFromCartAsync(string customerEmail, Gu return Result.Success(); } + public async Task> GetAmountOfTicketTypeAsync(Guid ticketTypeId) + { + long? amount; + + try + { + amount = await _redisService.GetLongValueAsync(GetAmountKey(ticketTypeId)); + } + catch (Exception e) + { + return Result.Failure(StatusCodes.Status500InternalServerError, e.Message); + } + + if (amount is null) + { + return Result.Failure(StatusCodes.Status500InternalServerError, "the amount of tickets could not be retrieved"); + } + + return Result.Success(amount.Value); + } + + public async Task SetAmountOfTicketTypeAsync(Guid ticketTypeId, long amount) + { + bool success; + + try + { + success = await _redisService.SetLongValueAsync(GetAmountKey(ticketTypeId), amount); + } + catch (Exception e) + { + return Result.Failure(StatusCodes.Status500InternalServerError, e.Message); + } + + if (!success) + { + return Result.Failure(StatusCodes.Status500InternalServerError, "the amount of tickets could not be updated"); + } + + return Result.Success(); + } + + public async Task> IncrementAmountOfTicketTypeAsync(Guid ticketTypeId, long amount) + { + long? newAmount; + + try + { + newAmount = await _redisService.IncrementValueAsync(GetAmountKey(ticketTypeId)); + } + catch (Exception e) + { + return Result.Failure(StatusCodes.Status500InternalServerError, e.Message); + } + + return Result.Success(newAmount.Value); + } + + public async Task> DecrementAmountOfTicketTypeAsync(Guid ticketTypeId, long amount) + { + long? newAmount; + + try + { + newAmount = await _redisService.DecrementValueAsync(GetAmountKey(ticketTypeId)); + } + catch (Exception e) + { + return Result.Failure(StatusCodes.Status500InternalServerError, e.Message); + } + + return Result.Success(newAmount.Value); + } + + public async Task RemoveAmountOfTicketTypeAsync(Guid ticketTypeId) + { + bool success; + + try + { + success = await _redisService.DeleteKeyAsync(GetAmountKey(ticketTypeId)); + } + catch (Exception e) + { + return Result.Failure(StatusCodes.Status500InternalServerError, e.Message); + } + + if (!success) + { + return Result.Failure(StatusCodes.Status500InternalServerError, "the amount of tickets could not be updated"); + } + + return Result.Success(); + } + private static string GetCartKey(string customerEmail) { return $"cart:{customerEmail}"; } + + private static string GetAmountKey(Guid ticketTypeId) + { + return $"amount:{ticketTypeId}"; + } } \ No newline at end of file From c1097b366b7721c3be2f814f4b83d51cc1e4e4aa Mon Sep 17 00:00:00 2001 From: kubapoke Date: Thu, 22 May 2025 20:25:43 +0200 Subject: [PATCH 65/80] added reserved ticket counting --- .../Events/Services/EventServiceTests.cs | 2 +- .../Tickets/Services/TicketServiceTests.cs | 53 +++++++++++++------ .../TickAPI/Events/Services/EventService.cs | 22 ++++++-- .../Repositories/ShoppingCartRepository.cs | 14 +++++ .../Tickets/Abstractions/ITicketService.cs | 2 +- .../TickAPI/Tickets/Services/TicketService.cs | 19 +++++-- 6 files changed, 87 insertions(+), 25 deletions(-) diff --git a/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs b/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs index 34b23ca..a4ac30b 100644 --- a/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs @@ -631,7 +631,7 @@ public async Task GetEventDetailsAsync_WhenSuccessful_ShouldReturnEventDetails() .ReturnsAsync(Result.Success(@event)); ticketServiceMock - .Setup(m => m.GetNumberOfAvailableTicketsByType(It.IsAny())) + .Setup(m => m.GetNumberOfAvailableTicketsByTypeAsync(It.IsAny())) .Returns((TicketType input) => Result.Success((uint)(input.Price / 10)) ); diff --git a/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs b/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs index aeaae9a..1c100de 100644 --- a/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs @@ -10,6 +10,7 @@ using TickAPI.Customers.Models; using TickAPI.Events.Models; using TickAPI.Organizers.Models; +using TickAPI.ShoppingCarts.Abstractions; using TickAPI.Tickets.Abstractions; using TickAPI.Tickets.DTOs.Response; using TickAPI.Tickets.Models; @@ -22,7 +23,7 @@ namespace TickAPI.Tests.Tickets.Services; public class TicketServiceTests { [Fact] - public void GetNumberOfAvailableTicketsByType_AmountsAreCorrect_ShouldReturnCorrectNumberOfTickets() + public async Task GetNumberOfAvailableTicketsByType_AmountsAreCorrect_ShouldReturnCorrectNumberOfTickets() { // Arrange var type = new TicketType { MaxCount = 30 }; @@ -30,6 +31,7 @@ public void GetNumberOfAvailableTicketsByType_AmountsAreCorrect_ShouldReturnCorr var ticketRepositoryMock = new Mock(); var ticketTypeRepositoryMock = new Mock(); + var shoppingCartRepositoryMock = new Mock(); var paginationServiceMock = new Mock(); var qrServiceMock = new Mock(); @@ -37,10 +39,11 @@ public void GetNumberOfAvailableTicketsByType_AmountsAreCorrect_ShouldReturnCorr .Setup(m => m.GetAllTicketsByTicketType(type)) .Returns(ticketList.AsQueryable()); - var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, + shoppingCartRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act - var result = sut.GetNumberOfAvailableTicketsByType(type); + var result = await sut.GetNumberOfAvailableTicketsByTypeAsync(type); // Assert Assert.True(result.IsSuccess); @@ -48,7 +51,7 @@ public void GetNumberOfAvailableTicketsByType_AmountsAreCorrect_ShouldReturnCorr } [Fact] - public void GetNumberOfAvailableTicketsByType_WhenMoreTicketExistThanMaxCount_ShouldReturnError() + public async Task GetNumberOfAvailableTicketsByType_WhenMoreTicketExistThanMaxCount_ShouldReturnError() { // Arrange var type = new TicketType { MaxCount = 30 }; @@ -56,6 +59,7 @@ public void GetNumberOfAvailableTicketsByType_WhenMoreTicketExistThanMaxCount_Sh var ticketRepositoryMock = new Mock(); var ticketTypeRepositoryMock = new Mock(); + var shoppingCartRepositoryMock = new Mock(); var paginationServiceMock = new Mock(); var qrServiceMock = new Mock(); @@ -63,10 +67,11 @@ public void GetNumberOfAvailableTicketsByType_WhenMoreTicketExistThanMaxCount_Sh .Setup(m => m.GetAllTicketsByTicketType(type)) .Returns(ticketList.AsQueryable()); - var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, + shoppingCartRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act - var result = sut.GetNumberOfAvailableTicketsByType(type); + var result = await sut.GetNumberOfAvailableTicketsByTypeAsync(type); // Assert Assert.True(result.IsError); @@ -115,6 +120,7 @@ public async Task GetTicketsForResellAsync_WhenDataIsValid_ShouldReturnSuccess() .Returns(allTickets); var ticketTypeRepositoryMock = new Mock(); + var shoppingCartRepositoryMock = new Mock(); var paginatedTickets = new PaginatedData( new List { ticket1, ticket2 }, @@ -161,7 +167,8 @@ public async Task GetTicketsForResellAsync_WhenDataIsValid_ShouldReturnSuccess() var qrServiceMock = new Mock(); - var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, + shoppingCartRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act var result = await sut.GetTicketsForResellAsync(eventId, page, pageSize); @@ -212,6 +219,7 @@ public async Task GetTicketsForResellAsync_WhenNoTicketsForResell_ShouldReturnEm .Returns(tickets); var ticketTypeRepositoryMock = new Mock(); + var shoppingCartRepositoryMock = new Mock(); var paginatedData = new PaginatedData( new List(), @@ -241,7 +249,8 @@ public async Task GetTicketsForResellAsync_WhenNoTicketsForResell_ShouldReturnEm .Returns(mappedData); var qrServiceMock = new Mock(); - var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, + shoppingCartRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act var result = await sut.GetTicketsForResellAsync(eventId, page, pageSize); @@ -281,6 +290,7 @@ public async Task GetTicketsForResellAsync_WhenPaginationFails_ShouldPropagateEr .Returns(tickets); var ticketTypeRepositoryMock = new Mock(); + var shoppingCartRepositoryMock = new Mock(); var paginationServiceMock = new Mock(); paginationServiceMock.Setup(p => p.PaginateAsync(It.IsAny>(), pageSize, page)) @@ -288,7 +298,8 @@ public async Task GetTicketsForResellAsync_WhenPaginationFails_ShouldPropagateEr var qrServiceMock = new Mock(); - var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, + shoppingCartRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act var result = await sut.GetTicketsForResellAsync(eventId, page, pageSize); @@ -314,6 +325,7 @@ public async Task GetTicketsForResellAsync_WhenNoTicketsForEvent_ShouldReturnEmp .Returns(tickets); var ticketTypeRepositoryMock = new Mock(); + var shoppingCartRepositoryMock = new Mock(); var paginatedData = new PaginatedData( new List(), @@ -343,7 +355,8 @@ public async Task GetTicketsForResellAsync_WhenNoTicketsForEvent_ShouldReturnEmp .Returns(mappedData); var qrServiceMock = new Mock(); - var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, + shoppingCartRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act var result = await sut.GetTicketsForResellAsync(eventId, page, pageSize); @@ -395,6 +408,7 @@ public async Task GetTicketDetailsAsync_WhenTicketExistsForTheUser_ShouldReturnT const string scanurl = "http://localhost"; var ticketRepositoryMock = new Mock(); var ticketTypeRepositoryMock = new Mock(); + var shoppingCartRepositoryMock = new Mock(); var paginationServiceMock = new Mock(); @@ -404,7 +418,8 @@ public async Task GetTicketDetailsAsync_WhenTicketExistsForTheUser_ShouldReturnT var qrServiceMock = new Mock(); qrServiceMock.Setup(m => m.GenerateQrCode(scanurl)).Returns([]); - var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, + shoppingCartRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act @@ -447,10 +462,12 @@ public async Task GetTicketDetailsAsync_WhenTicketDoesNotExistForTheUser_ShouldR ReturnsAsync(Result.Failure(StatusCodes.Status404NotFound, "Ticket with this id doesn't exist " + "for this user")); var ticketTypeRepositoryMock = new Mock(); + var shoppingCartRepositoryMock = new Mock(); var paginationServiceMock = new Mock(); var qrServiceMock = new Mock(); - var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, + shoppingCartRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act @@ -527,6 +544,7 @@ public async Task GetTicketsForCustomerAsync_WithValidInput_ReturnsSuccessResult var ticketRepositoryMock = new Mock(); ticketRepositoryMock.Setup(r => r.GetTicketsByCustomerEmail(email)).Returns(tickets.AsQueryable()); var ticketTypeRepositoryMock = new Mock(); + var shoppingCartRepositoryMock = new Mock(); var paginationServiceMock = new Mock(); paginationServiceMock.Setup(p => p.PaginateAsync(tickets.AsQueryable(), pageSize, page)) @@ -537,7 +555,8 @@ public async Task GetTicketsForCustomerAsync_WithValidInput_ReturnsSuccessResult var qrServiceMock = new Mock(); - var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, + shoppingCartRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act var result = await sut.GetTicketsForCustomerAsync(email, page, pageSize); @@ -571,6 +590,7 @@ public async Task GetTicketsForCustomerAsync_WhenUserHasNoTickets_ReturnsEmptyPa ticketRepositoryMock.Setup(r => r.GetTicketsByCustomerEmail(email)).Returns(emptyTickets.AsQueryable()); var ticketTypeRepositoryMock = new Mock(); + var shoppingCartRepositoryMock = new Mock(); var paginationServiceMock = new Mock(); paginationServiceMock.Setup(p => p.PaginateAsync(emptyTickets.AsQueryable(), pageSize, page)).ReturnsAsync(paginatedResult); @@ -579,7 +599,8 @@ public async Task GetTicketsForCustomerAsync_WhenUserHasNoTickets_ReturnsEmptyPa var qrServiceMock = new Mock(); - var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, + shoppingCartRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act var result = await sut.GetTicketsForCustomerAsync(email, page, pageSize); @@ -597,9 +618,11 @@ public async Task ScanTicket_WhenScanningSuccesful_ShouldReturnSuccess() var ticketRepositoryMock = new Mock(); ticketRepositoryMock.Setup(m => m.MarkTicketAsUsed(guid)).ReturnsAsync(Result.Success()); var ticketTypeRepositoryMock = new Mock(); + var shoppingCartRepositoryMock = new Mock(); var paginationServiceMock = new Mock(); var qrServiceMock = new Mock(); - var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, + shoppingCartRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); // Act var res = await sut.ScanTicket(guid); diff --git a/TickAPI/TickAPI/Events/Services/EventService.cs b/TickAPI/TickAPI/Events/Services/EventService.cs index 39f7807..350a4e0 100644 --- a/TickAPI/TickAPI/Events/Services/EventService.cs +++ b/TickAPI/TickAPI/Events/Services/EventService.cs @@ -133,10 +133,24 @@ public async Task> GetEventDetailsAsync(Guid ? ev.Categories.Select((c) => new GetEventResponseCategoryDto(c.Name)).ToList() : new List(); - var ticketTypes = ev.TicketTypes.Count > 0 - ? ev.TicketTypes.Select((t) => new GetEventDetailsResponseTicketTypeDto(t.Id, t.Description, t.Price, - t.Currency, t.AvailableFrom, _ticketService.GetNumberOfAvailableTicketsByType(t).Value)).ToList() - : new List(); + var ticketTypes = new List(); + + if (ev.TicketTypes.Count > 0) + { + foreach (var t in ev.TicketTypes) + { + var availableCount = await _ticketService.GetNumberOfAvailableTicketsByTypeAsync(t); + + ticketTypes.Add(new GetEventDetailsResponseTicketTypeDto( + t.Id, + t.Description, + t.Price, + t.Currency, + t.AvailableFrom, + availableCount.Value + )); + } + } var address = new GetEventResponseAddressDto(ev.Address.Country, ev.Address.City, ev.Address.PostalCode, ev.Address.Street, ev.Address.HouseNumber, ev.Address.FlatNumber); diff --git a/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs b/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs index 2b15778..8699445 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs @@ -86,6 +86,13 @@ public async Task AddNewTicketsToCartAsync(string customerEmail, Guid ti }); } + var incrementTicketAmountResult = await IncrementAmountOfTicketTypeAsync(ticketTypeId, amount); + + if (incrementTicketAmountResult.IsError) + { + return Result.PropagateError(incrementTicketAmountResult); + } + var updateShoppingCartResult = await UpdateShoppingCartAsync(customerEmail, cart); if (updateShoppingCartResult.IsError) @@ -131,6 +138,13 @@ public async Task RemoveNewTicketsFromCartAsync(string customerEmail, Gu { cart.NewTickets.Remove(existingEntry); } + + var decrementTicketAmountResult = await DecrementAmountOfTicketTypeAsync(ticketTypeId, amount); + + if (decrementTicketAmountResult.IsError) + { + return Result.PropagateError(decrementTicketAmountResult); + } var updateShoppingCartResult = await UpdateShoppingCartAsync(customerEmail, cart); diff --git a/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs b/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs index 8b75bd3..303a4ab 100644 --- a/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs +++ b/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs @@ -10,7 +10,7 @@ namespace TickAPI.Tickets.Abstractions; public interface ITicketService { - public Result GetNumberOfAvailableTicketsByType(TicketType ticketType); + public Task> GetNumberOfAvailableTicketsByTypeAsync(TicketType ticketType); public Task> GetNumberOfAvailableTicketsByTypeIdAsync(Guid ticketTypeId); public Task> CheckTicketAvailabilityByTypeIdAsync(Guid ticketTypeId, uint amount); public Task>> GetTicketsForResellAsync(Guid eventId, int page, diff --git a/TickAPI/TickAPI/Tickets/Services/TicketService.cs b/TickAPI/TickAPI/Tickets/Services/TicketService.cs index e141438..5d1cd5e 100644 --- a/TickAPI/TickAPI/Tickets/Services/TicketService.cs +++ b/TickAPI/TickAPI/Tickets/Services/TicketService.cs @@ -6,6 +6,7 @@ using TickAPI.Common.Results.Generic; using TickAPI.Customers.Abstractions; using TickAPI.Customers.Models; +using TickAPI.ShoppingCarts.Abstractions; using TickAPI.Tickets.Abstractions; using TickAPI.Tickets.DTOs.Request; using TickAPI.Tickets.DTOs.Response; @@ -20,24 +21,34 @@ public class TicketService : ITicketService { private readonly ITicketRepository _ticketRepository; private readonly ITicketTypeRepository _ticketTypeRepository; + private readonly IShoppingCartRepository _shoppingCartRepository; private readonly IPaginationService _paginationService; private readonly IQRCodeService _qrCodeService; public TicketService(ITicketRepository ticketRepository, ITicketTypeRepository ticketTypeRepository, - IPaginationService paginationService, IQRCodeService qrCodeService) + IShoppingCartRepository shoppingCartRepository, IPaginationService paginationService, IQRCodeService qrCodeService) { _ticketRepository = ticketRepository; _ticketTypeRepository = ticketTypeRepository; + _shoppingCartRepository = shoppingCartRepository; _paginationService = paginationService; _qrCodeService = qrCodeService; } // TODO: Update this method to also count tickets cached in Redis as unavailable - public Result GetNumberOfAvailableTicketsByType(TicketType ticketType) + public async Task> GetNumberOfAvailableTicketsByTypeAsync(TicketType ticketType) { var unavailableTickets = _ticketRepository.GetAllTicketsByTicketType(ticketType); + var reservedTicketsAmountResult = await _shoppingCartRepository.GetAmountOfTicketTypeAsync(ticketType.Id); + + if (reservedTicketsAmountResult.IsError) + { + return Result.PropagateError(reservedTicketsAmountResult); + } + + var reservedTicketsAmount = reservedTicketsAmountResult.Value; - var availableCount = ticketType.MaxCount - unavailableTickets.Count(); + var availableCount = ticketType.MaxCount - unavailableTickets.Count() - reservedTicketsAmount; if (availableCount < 0) { @@ -57,7 +68,7 @@ public async Task> GetNumberOfAvailableTicketsByTypeIdAsync(Guid ti return Result.PropagateError(ticketTypeResult); } - return GetNumberOfAvailableTicketsByType(ticketTypeResult.Value!); + return await GetNumberOfAvailableTicketsByTypeAsync(ticketTypeResult.Value!); } public async Task> CheckTicketAvailabilityByTypeIdAsync(Guid ticketTypeId, uint amount) From ec35bc9834401eacf57b1b0755bc5d648bcb9874 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Thu, 22 May 2025 23:17:00 +0200 Subject: [PATCH 66/80] fixed counting amount of reserved tickets --- TickAPI/TickAPI/Events/Services/EventService.cs | 9 +++++++-- .../ShoppingCarts/Repositories/ShoppingCartRepository.cs | 6 +++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/TickAPI/TickAPI/Events/Services/EventService.cs b/TickAPI/TickAPI/Events/Services/EventService.cs index 350a4e0..91cdda2 100644 --- a/TickAPI/TickAPI/Events/Services/EventService.cs +++ b/TickAPI/TickAPI/Events/Services/EventService.cs @@ -139,7 +139,12 @@ public async Task> GetEventDetailsAsync(Guid { foreach (var t in ev.TicketTypes) { - var availableCount = await _ticketService.GetNumberOfAvailableTicketsByTypeAsync(t); + var availableCountResult = await _ticketService.GetNumberOfAvailableTicketsByTypeAsync(t); + + if (availableCountResult.IsError) + { + return Result.PropagateError(availableCountResult); + } ticketTypes.Add(new GetEventDetailsResponseTicketTypeDto( t.Id, @@ -147,7 +152,7 @@ public async Task> GetEventDetailsAsync(Guid t.Price, t.Currency, t.AvailableFrom, - availableCount.Value + availableCountResult.Value )); } } diff --git a/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs b/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs index 8699445..a487c63 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs @@ -171,7 +171,7 @@ public async Task> GetAmountOfTicketTypeAsync(Guid ticketTypeId) if (amount is null) { - return Result.Failure(StatusCodes.Status500InternalServerError, "the amount of tickets could not be retrieved"); + return Result.Success(0); } return Result.Success(amount.Value); @@ -204,7 +204,7 @@ public async Task> IncrementAmountOfTicketTypeAsync(Guid ticketType try { - newAmount = await _redisService.IncrementValueAsync(GetAmountKey(ticketTypeId)); + newAmount = await _redisService.IncrementValueAsync(GetAmountKey(ticketTypeId), amount); } catch (Exception e) { @@ -220,7 +220,7 @@ public async Task> DecrementAmountOfTicketTypeAsync(Guid ticketType try { - newAmount = await _redisService.DecrementValueAsync(GetAmountKey(ticketTypeId)); + newAmount = await _redisService.DecrementValueAsync(GetAmountKey(ticketTypeId), amount); } catch (Exception e) { From 98ddd7407c112000b7ad7d57a8b2178c168b49ed Mon Sep 17 00:00:00 2001 From: kubapoke Date: Fri, 23 May 2025 22:35:59 +0200 Subject: [PATCH 67/80] added redis service method for retrieving keys --- .../TickAPI/Common/Redis/Abstractions/IRedisService.cs | 1 + TickAPI/TickAPI/Common/Redis/Services/RedisService.cs | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/TickAPI/TickAPI/Common/Redis/Abstractions/IRedisService.cs b/TickAPI/TickAPI/Common/Redis/Abstractions/IRedisService.cs index 194a7d5..2dd6674 100644 --- a/TickAPI/TickAPI/Common/Redis/Abstractions/IRedisService.cs +++ b/TickAPI/TickAPI/Common/Redis/Abstractions/IRedisService.cs @@ -13,4 +13,5 @@ public interface IRedisService public Task DecrementValueAsync(string key, long value = 1); public Task GetLongValueAsync(string key); public Task SetLongValueAsync(string key, long value, TimeSpan? expiry = null); + public Task> GetKeysByPatternAsync(string pattern); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Redis/Services/RedisService.cs b/TickAPI/TickAPI/Common/Redis/Services/RedisService.cs index bba9b34..9c2b0fa 100644 --- a/TickAPI/TickAPI/Common/Redis/Services/RedisService.cs +++ b/TickAPI/TickAPI/Common/Redis/Services/RedisService.cs @@ -6,11 +6,13 @@ namespace TickAPI.Common.Redis.Services; public class RedisService : IRedisService { + private readonly IConnectionMultiplexer _connectionMultiplexer; private readonly IDatabase _database; private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web); public RedisService(IConnectionMultiplexer connectionMultiplexer) { + _connectionMultiplexer = connectionMultiplexer; _database = connectionMultiplexer.GetDatabase(); } @@ -85,6 +87,12 @@ public async Task SetLongValueAsync(string key, long value, TimeSpan? expi return await RetryAsync(async () => await _database.StringSetAsync(key, value.ToString(), expiry)); } + public async Task> GetKeysByPatternAsync(string pattern) + { + var server = _connectionMultiplexer.GetServer(_connectionMultiplexer.GetEndPoints().First()); + return server.Keys(pattern: pattern).Select(k => k.ToString()); + } + private static async Task RetryAsync(Func> action, int retryCount = 3, int millisecondsDelay = 100) { var attempt = 0; From bc6d27aafa71ad74250c4f01722280895b78adb0 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Fri, 23 May 2025 23:26:03 +0200 Subject: [PATCH 68/80] added background service for syncing cart counters with their actual state --- TickAPI/TickAPI/Program.cs | 2 + .../ShoppingCartSyncBackgroundService.cs | 80 +++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 TickAPI/TickAPI/ShoppingCarts/Background/ShoppingCartSyncBackgroundService.cs diff --git a/TickAPI/TickAPI/Program.cs b/TickAPI/TickAPI/Program.cs index 7d05eb3..17ed802 100644 --- a/TickAPI/TickAPI/Program.cs +++ b/TickAPI/TickAPI/Program.cs @@ -45,6 +45,7 @@ using TickAPI.Common.QR.Abstractions; using TickAPI.Common.QR.Services; using TickAPI.ShoppingCarts.Abstractions; +using TickAPI.ShoppingCarts.Background; using TickAPI.ShoppingCarts.Repositories; using TickAPI.ShoppingCarts.Services; using TickAPI.TicketTypes.Abstractions; @@ -126,6 +127,7 @@ // Add shopping cart services. builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddHostedService(); // Add ticket type services builder.Services.AddScoped(); diff --git a/TickAPI/TickAPI/ShoppingCarts/Background/ShoppingCartSyncBackgroundService.cs b/TickAPI/TickAPI/ShoppingCarts/Background/ShoppingCartSyncBackgroundService.cs new file mode 100644 index 0000000..fd66407 --- /dev/null +++ b/TickAPI/TickAPI/ShoppingCarts/Background/ShoppingCartSyncBackgroundService.cs @@ -0,0 +1,80 @@ +using TickAPI.Common.Redis.Abstractions; +using TickAPI.ShoppingCarts.Models; + +namespace TickAPI.ShoppingCarts.Background; + +public class ShoppingCartSyncBackgroundService : BackgroundService +{ + private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; + private static readonly TimeSpan SyncInterval = TimeSpan.FromMinutes(5); + + public ShoppingCartSyncBackgroundService(IServiceProvider serviceProvider, ILogger logger) + { + _serviceProvider = serviceProvider; + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + try + { + using var scope = _serviceProvider.CreateScope(); + var redisService = scope.ServiceProvider.GetRequiredService(); + await SyncTicketTypeCountersAsync(redisService, stoppingToken); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while syncing shopping cart ticket counters"); + } + + await Task.Delay(SyncInterval, stoppingToken); + } + } + + private async Task SyncTicketTypeCountersAsync(IRedisService redisService, CancellationToken cancellationToken) + { + var cartKeys = await redisService.GetKeysByPatternAsync("cart:*"); + var ticketTypeCounts = new Dictionary(); + + foreach (var cartKey in cartKeys) + { + var cart = await redisService.GetObjectAsync(cartKey); + if (cart == null) continue; + + foreach (var ticket in cart.NewTickets) + { + if (ticketTypeCounts.ContainsKey(ticket.TicketTypeId)) + ticketTypeCounts[ticket.TicketTypeId] += ticket.Quantity; + else + ticketTypeCounts[ticket.TicketTypeId] = ticket.Quantity; + } + + if (cancellationToken.IsCancellationRequested) return; + } + + foreach (var kvp in ticketTypeCounts) + { + await redisService.SetLongValueAsync($"amount:{kvp.Key}", kvp.Value); + } + + var existingAmountKeys = await redisService.GetKeysByPatternAsync("amount:*"); + + foreach (var key in existingAmountKeys) + { + var typeIdStr = key.Split(":").Last(); + if (!Guid.TryParse(typeIdStr, out var ticketTypeId)) continue; + + if (!ticketTypeCounts.ContainsKey(ticketTypeId) || ticketTypeCounts[ticketTypeId] == 0) + { + await redisService.DeleteKeyAsync(key); + } + + if (cancellationToken.IsCancellationRequested) return; + } + + _logger.LogInformation("Synchronized ticket counters for {Count} ticket types", ticketTypeCounts.Count); + } +} From a9bc3278117a8beb19c5fdf4423823370be3b68e Mon Sep 17 00:00:00 2001 From: kubapoke Date: Fri, 23 May 2025 23:43:02 +0200 Subject: [PATCH 69/80] fixed tests to account for changed method logic --- .../TickAPI.Tests/Events/Services/EventServiceTests.cs | 2 +- .../TickAPI.Tests/Tickets/Services/TicketServiceTests.cs | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs b/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs index a4ac30b..464170b 100644 --- a/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs @@ -632,7 +632,7 @@ public async Task GetEventDetailsAsync_WhenSuccessful_ShouldReturnEventDetails() ticketServiceMock .Setup(m => m.GetNumberOfAvailableTicketsByTypeAsync(It.IsAny())) - .Returns((TicketType input) => + .ReturnsAsync((TicketType input) => Result.Success((uint)(input.Price / 10)) ); diff --git a/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs b/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs index 1c100de..b6f1058 100644 --- a/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs @@ -39,6 +39,10 @@ public async Task GetNumberOfAvailableTicketsByType_AmountsAreCorrect_ShouldRetu .Setup(m => m.GetAllTicketsByTicketType(type)) .Returns(ticketList.AsQueryable()); + shoppingCartRepositoryMock + .Setup(s => s.GetAmountOfTicketTypeAsync(type.Id)) + .ReturnsAsync(Result.Success(0)); + var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, shoppingCartRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); @@ -66,6 +70,10 @@ public async Task GetNumberOfAvailableTicketsByType_WhenMoreTicketExistThanMaxCo ticketRepositoryMock .Setup(m => m.GetAllTicketsByTicketType(type)) .Returns(ticketList.AsQueryable()); + + shoppingCartRepositoryMock + .Setup(s => s.GetAmountOfTicketTypeAsync(type.Id)) + .ReturnsAsync(Result.Success(0)); var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, shoppingCartRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); From 45788f9c2dfa0c89131f1f47c87739ab515f4888 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Sat, 24 May 2025 12:34:31 +0200 Subject: [PATCH 70/80] Logic for setting ticket state to resell --- .../Tickets/Abstractions/ITicketRepository.cs | 1 + .../Tickets/Abstractions/ITicketService.cs | 2 ++ .../Tickets/Controllers/TicketsController.cs | 13 +++++++++ .../DTOs/Request/SetTicketForResellDataDto.cs | 6 ++++ TickAPI/TickAPI/Tickets/Models/Ticket.cs | 1 + .../Tickets/Repositories/TicketRepository.cs | 13 +++++++++ .../TickAPI/Tickets/Services/TicketService.cs | 29 +++++++++++++++++-- 7 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 TickAPI/TickAPI/Tickets/DTOs/Request/SetTicketForResellDataDto.cs diff --git a/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs b/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs index 6290950..6bdacf6 100644 --- a/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs +++ b/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs @@ -12,4 +12,5 @@ public interface ITicketRepository public IQueryable GetTicketsByEventId(Guid eventId); public IQueryable GetTicketsByCustomerEmail(string email); public Task MarkTicketAsUsed(Guid id); + public Task SetTicketForResell(Guid ticketId, decimal newPrice); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs b/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs index c722a8c..f06b2c5 100644 --- a/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs +++ b/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs @@ -17,4 +17,6 @@ public Task>> GetTicketsForCustome public Task ScanTicket(Guid ticketGuid); public Task> GetTicketDetailsAsync(Guid ticketGuid, string email, string scanUrl); + + public Task SetTicketForResellAsync(Guid ticketId, string email, decimal resellPrice); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs b/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs index 77b45e2..a39dc32 100644 --- a/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs +++ b/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs @@ -63,5 +63,18 @@ public async Task> ScanTicket(Guid id) var res = await _ticketService.ScanTicket(id); return res.ToObjectResult(); } + + [HttpPost("resell/{id:guid}")] + + public async Task> SetTicketForResell(Guid id, [FromBody] SetTicketForResellDataDto data) + { + var emailResult = _claimsService.GetEmailFromClaims(User.Claims); + if (emailResult.IsError) + { + return emailResult.ToObjectResult(); + } + var res = await _ticketService.SetTicketForResellAsync(id, emailResult.Value!, data.ResellPrice); + return res.ToObjectResult(); + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/DTOs/Request/SetTicketForResellDataDto.cs b/TickAPI/TickAPI/Tickets/DTOs/Request/SetTicketForResellDataDto.cs new file mode 100644 index 0000000..0058278 --- /dev/null +++ b/TickAPI/TickAPI/Tickets/DTOs/Request/SetTicketForResellDataDto.cs @@ -0,0 +1,6 @@ +namespace TickAPI.Tickets.DTOs.Request; + +public record SetTicketForResellDataDto +( + decimal ResellPrice +); \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Models/Ticket.cs b/TickAPI/TickAPI/Tickets/Models/Ticket.cs index 49a68ac..fdd8770 100644 --- a/TickAPI/TickAPI/Tickets/Models/Ticket.cs +++ b/TickAPI/TickAPI/Tickets/Models/Ticket.cs @@ -11,5 +11,6 @@ public class Ticket public string NameOnTicket { get; set; } public string? Seats { get; set; } public bool ForResell { get; set; } + public decimal? ResellPrice { get; set; } public bool Used { get; set; } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs b/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs index e5abb5e..e86075b 100644 --- a/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs +++ b/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs @@ -66,4 +66,17 @@ public async Task MarkTicketAsUsed(Guid id) await _tickApiDbContext.SaveChangesAsync(); return Result.Success(); } + + public async Task SetTicketForResell(Guid ticketId, decimal newPrice) + { + var ticket = await _tickApiDbContext.Tickets.FirstOrDefaultAsync(t => t.Id == ticketId); + if (ticket == null) + { + return Result.Failure(StatusCodes.Status404NotFound, "Ticket with this id doesn't exist"); + } + ticket.ForResell = true; + ticket.ResellPrice = newPrice; + await _tickApiDbContext.SaveChangesAsync(); + return Result.Success(); + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Services/TicketService.cs b/TickAPI/TickAPI/Tickets/Services/TicketService.cs index 27b60fd..b16e64e 100644 --- a/TickAPI/TickAPI/Tickets/Services/TicketService.cs +++ b/TickAPI/TickAPI/Tickets/Services/TicketService.cs @@ -110,6 +110,31 @@ public async Task ScanTicket(Guid ticketGuid) var res = await _ticketRepository.MarkTicketAsUsed(ticketGuid); return res; } - - + + public async Task SetTicketForResellAsync(Guid ticketId, string email, decimal resellPrice) + { + if (resellPrice <= 0) + { + return Result.Failure(StatusCodes.Status500InternalServerError, "Price must be greater than zero"); + } + var ticketRes = await _ticketRepository.GetTicketWithDetailsByIdAndEmailAsync(ticketId, email); + if (ticketRes.IsError) + { + return Result.PropagateError(ticketRes); + } + + if (ticketRes.Value!.Type.Price < 1.5m*resellPrice) + { + return Result.Failure(StatusCodes.Status500InternalServerError, "Resell price cannot exceed " + + "value of original price times 1.5"); + } + + if (ticketRes.Value!.ForResell) + { + return Result.Failure(StatusCodes.Status500InternalServerError, "Ticket is already set for resell"); + } + + var res = await _ticketRepository.SetTicketForResell(ticketId, resellPrice); + return res; + } } \ No newline at end of file From d898aa22c2065b17ce183ea705a1f5ca08c004ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Sat, 24 May 2025 13:08:08 +0200 Subject: [PATCH 71/80] Validation stuff --- .../20250524104655_resellprice.Designer.cs | 395 ++++++++++++++++++ .../Migrations/20250524104655_resellprice.cs | 28 ++ .../20250524105603_nullableowner.Designer.cs | 393 +++++++++++++++++ .../20250524105603_nullableowner.cs | 60 +++ .../TickApiDbContextModelSnapshot.cs | 9 +- TickAPI/TickAPI/Tickets/Models/Ticket.cs | 2 +- .../TickAPI/Tickets/Services/TicketService.cs | 7 +- 7 files changed, 888 insertions(+), 6 deletions(-) create mode 100644 TickAPI/TickAPI/Migrations/20250524104655_resellprice.Designer.cs create mode 100644 TickAPI/TickAPI/Migrations/20250524104655_resellprice.cs create mode 100644 TickAPI/TickAPI/Migrations/20250524105603_nullableowner.Designer.cs create mode 100644 TickAPI/TickAPI/Migrations/20250524105603_nullableowner.cs diff --git a/TickAPI/TickAPI/Migrations/20250524104655_resellprice.Designer.cs b/TickAPI/TickAPI/Migrations/20250524104655_resellprice.Designer.cs new file mode 100644 index 0000000..ad10aaa --- /dev/null +++ b/TickAPI/TickAPI/Migrations/20250524104655_resellprice.Designer.cs @@ -0,0 +1,395 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using TickAPI.Common.TickApiDbContext; + +#nullable disable + +namespace TickAPI.Migrations +{ + [DbContext(typeof(TickApiDbContext))] + [Migration("20250524104655_resellprice")] + partial class resellprice + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("CategoryEvent", b => + { + b.Property("CategoriesId") + .HasColumnType("uniqueidentifier"); + + b.Property("EventsId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("CategoriesId", "EventsId"); + + b.HasIndex("EventsId"); + + b.ToTable("CategoryEvent"); + }); + + modelBuilder.Entity("TickAPI.Addresses.Models.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("City") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Country") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FlatNumber") + .HasColumnType("bigint"); + + b.Property("HouseNumber") + .HasColumnType("bigint"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Street") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Addresses"); + }); + + modelBuilder.Entity("TickAPI.Admins.Models.Admin", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Login") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Admins"); + }); + + modelBuilder.Entity("TickAPI.Categories.Models.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + + b.HasData( + new + { + Id = new Guid("ec3daf69-baa9-4fcd-a674-c09884a57272"), + Name = "Music" + }, + new + { + Id = new Guid("de89dd76-3b29-43e1-8f4b-5278b1b8bde2"), + Name = "Sports" + }, + new + { + Id = new Guid("ea58370b-2a17-4770-abea-66399ad69fb8"), + Name = "Conferences" + }, + new + { + Id = new Guid("4a086d9e-59de-4fd1-a1b2-bd9b5eec797c"), + Name = "Theatre" + }, + new + { + Id = new Guid("5f8dbe65-30be-453f-8f22-191a11b2977b"), + Name = "Comedy" + }, + new + { + Id = new Guid("4421327a-4bc8-4706-bec0-666f78ed0c69"), + Name = "Workshops" + }); + }); + + modelBuilder.Entity("TickAPI.Customers.Models.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Event", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AddressId") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("EventStatus") + .HasColumnType("int"); + + b.Property("MinimumAge") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OrganizerId") + .HasColumnType("uniqueidentifier"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("AddressId"); + + b.HasIndex("OrganizerId"); + + b.ToTable("Events"); + }); + + modelBuilder.Entity("TickAPI.Organizers.Models.Organizer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsVerified") + .HasColumnType("bit"); + + b.Property("LastName") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Organizers"); + }); + + modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AvailableFrom") + .HasColumnType("datetime2"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EventId") + .HasColumnType("uniqueidentifier"); + + b.Property("MaxCount") + .HasColumnType("bigint"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("EventId"); + + b.ToTable("TicketTypes"); + }); + + modelBuilder.Entity("TickAPI.Tickets.Models.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ForResell") + .HasColumnType("bit"); + + b.Property("NameOnTicket") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("uniqueidentifier"); + + b.Property("ResellPrice") + .HasColumnType("decimal(18,2)"); + + b.Property("Seats") + .HasColumnType("nvarchar(max)"); + + b.Property("TypeId") + .HasColumnType("uniqueidentifier"); + + b.Property("Used") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("TypeId"); + + b.ToTable("Tickets"); + }); + + modelBuilder.Entity("CategoryEvent", b => + { + b.HasOne("TickAPI.Categories.Models.Category", null) + .WithMany() + .HasForeignKey("CategoriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TickAPI.Events.Models.Event", null) + .WithMany() + .HasForeignKey("EventsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Event", b => + { + b.HasOne("TickAPI.Addresses.Models.Address", "Address") + .WithMany() + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TickAPI.Organizers.Models.Organizer", "Organizer") + .WithMany("Events") + .HasForeignKey("OrganizerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Address"); + + b.Navigation("Organizer"); + }); + + modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => + { + b.HasOne("TickAPI.Events.Models.Event", "Event") + .WithMany("TicketTypes") + .HasForeignKey("EventId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Event"); + }); + + modelBuilder.Entity("TickAPI.Tickets.Models.Ticket", b => + { + b.HasOne("TickAPI.Customers.Models.Customer", "Owner") + .WithMany("Tickets") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TickAPI.TicketTypes.Models.TicketType", "Type") + .WithMany("Tickets") + .HasForeignKey("TypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + + b.Navigation("Type"); + }); + + modelBuilder.Entity("TickAPI.Customers.Models.Customer", b => + { + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Event", b => + { + b.Navigation("TicketTypes"); + }); + + modelBuilder.Entity("TickAPI.Organizers.Models.Organizer", b => + { + b.Navigation("Events"); + }); + + modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => + { + b.Navigation("Tickets"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TickAPI/TickAPI/Migrations/20250524104655_resellprice.cs b/TickAPI/TickAPI/Migrations/20250524104655_resellprice.cs new file mode 100644 index 0000000..db156f8 --- /dev/null +++ b/TickAPI/TickAPI/Migrations/20250524104655_resellprice.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TickAPI.Migrations +{ + /// + public partial class resellprice : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ResellPrice", + table: "Tickets", + type: "decimal(18,2)", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ResellPrice", + table: "Tickets"); + } + } +} diff --git a/TickAPI/TickAPI/Migrations/20250524105603_nullableowner.Designer.cs b/TickAPI/TickAPI/Migrations/20250524105603_nullableowner.Designer.cs new file mode 100644 index 0000000..5d1fbea --- /dev/null +++ b/TickAPI/TickAPI/Migrations/20250524105603_nullableowner.Designer.cs @@ -0,0 +1,393 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using TickAPI.Common.TickApiDbContext; + +#nullable disable + +namespace TickAPI.Migrations +{ + [DbContext(typeof(TickApiDbContext))] + [Migration("20250524105603_nullableowner")] + partial class nullableowner + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("CategoryEvent", b => + { + b.Property("CategoriesId") + .HasColumnType("uniqueidentifier"); + + b.Property("EventsId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("CategoriesId", "EventsId"); + + b.HasIndex("EventsId"); + + b.ToTable("CategoryEvent"); + }); + + modelBuilder.Entity("TickAPI.Addresses.Models.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("City") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Country") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FlatNumber") + .HasColumnType("bigint"); + + b.Property("HouseNumber") + .HasColumnType("bigint"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Street") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Addresses"); + }); + + modelBuilder.Entity("TickAPI.Admins.Models.Admin", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Login") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Admins"); + }); + + modelBuilder.Entity("TickAPI.Categories.Models.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + + b.HasData( + new + { + Id = new Guid("ec3daf69-baa9-4fcd-a674-c09884a57272"), + Name = "Music" + }, + new + { + Id = new Guid("de89dd76-3b29-43e1-8f4b-5278b1b8bde2"), + Name = "Sports" + }, + new + { + Id = new Guid("ea58370b-2a17-4770-abea-66399ad69fb8"), + Name = "Conferences" + }, + new + { + Id = new Guid("4a086d9e-59de-4fd1-a1b2-bd9b5eec797c"), + Name = "Theatre" + }, + new + { + Id = new Guid("5f8dbe65-30be-453f-8f22-191a11b2977b"), + Name = "Comedy" + }, + new + { + Id = new Guid("4421327a-4bc8-4706-bec0-666f78ed0c69"), + Name = "Workshops" + }); + }); + + modelBuilder.Entity("TickAPI.Customers.Models.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Event", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AddressId") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("EventStatus") + .HasColumnType("int"); + + b.Property("MinimumAge") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OrganizerId") + .HasColumnType("uniqueidentifier"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("AddressId"); + + b.HasIndex("OrganizerId"); + + b.ToTable("Events"); + }); + + modelBuilder.Entity("TickAPI.Organizers.Models.Organizer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsVerified") + .HasColumnType("bit"); + + b.Property("LastName") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Organizers"); + }); + + modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AvailableFrom") + .HasColumnType("datetime2"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EventId") + .HasColumnType("uniqueidentifier"); + + b.Property("MaxCount") + .HasColumnType("bigint"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("EventId"); + + b.ToTable("TicketTypes"); + }); + + modelBuilder.Entity("TickAPI.Tickets.Models.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ForResell") + .HasColumnType("bit"); + + b.Property("NameOnTicket") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("uniqueidentifier"); + + b.Property("ResellPrice") + .HasColumnType("decimal(18,2)"); + + b.Property("Seats") + .HasColumnType("nvarchar(max)"); + + b.Property("TypeId") + .HasColumnType("uniqueidentifier"); + + b.Property("Used") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("TypeId"); + + b.ToTable("Tickets"); + }); + + modelBuilder.Entity("CategoryEvent", b => + { + b.HasOne("TickAPI.Categories.Models.Category", null) + .WithMany() + .HasForeignKey("CategoriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TickAPI.Events.Models.Event", null) + .WithMany() + .HasForeignKey("EventsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Event", b => + { + b.HasOne("TickAPI.Addresses.Models.Address", "Address") + .WithMany() + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TickAPI.Organizers.Models.Organizer", "Organizer") + .WithMany("Events") + .HasForeignKey("OrganizerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Address"); + + b.Navigation("Organizer"); + }); + + modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => + { + b.HasOne("TickAPI.Events.Models.Event", "Event") + .WithMany("TicketTypes") + .HasForeignKey("EventId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Event"); + }); + + modelBuilder.Entity("TickAPI.Tickets.Models.Ticket", b => + { + b.HasOne("TickAPI.Customers.Models.Customer", "Owner") + .WithMany("Tickets") + .HasForeignKey("OwnerId"); + + b.HasOne("TickAPI.TicketTypes.Models.TicketType", "Type") + .WithMany("Tickets") + .HasForeignKey("TypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + + b.Navigation("Type"); + }); + + modelBuilder.Entity("TickAPI.Customers.Models.Customer", b => + { + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Event", b => + { + b.Navigation("TicketTypes"); + }); + + modelBuilder.Entity("TickAPI.Organizers.Models.Organizer", b => + { + b.Navigation("Events"); + }); + + modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => + { + b.Navigation("Tickets"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TickAPI/TickAPI/Migrations/20250524105603_nullableowner.cs b/TickAPI/TickAPI/Migrations/20250524105603_nullableowner.cs new file mode 100644 index 0000000..b868a03 --- /dev/null +++ b/TickAPI/TickAPI/Migrations/20250524105603_nullableowner.cs @@ -0,0 +1,60 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TickAPI.Migrations +{ + /// + public partial class nullableowner : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Tickets_Customers_OwnerId", + table: "Tickets"); + + migrationBuilder.AlterColumn( + name: "OwnerId", + table: "Tickets", + type: "uniqueidentifier", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uniqueidentifier"); + + migrationBuilder.AddForeignKey( + name: "FK_Tickets_Customers_OwnerId", + table: "Tickets", + column: "OwnerId", + principalTable: "Customers", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Tickets_Customers_OwnerId", + table: "Tickets"); + + migrationBuilder.AlterColumn( + name: "OwnerId", + table: "Tickets", + type: "uniqueidentifier", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), + oldClrType: typeof(Guid), + oldType: "uniqueidentifier", + oldNullable: true); + + migrationBuilder.AddForeignKey( + name: "FK_Tickets_Customers_OwnerId", + table: "Tickets", + column: "OwnerId", + principalTable: "Customers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs b/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs index 50fd0cc..c6fa5bc 100644 --- a/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs +++ b/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs @@ -279,9 +279,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("nvarchar(max)"); - b.Property("OwnerId") + b.Property("OwnerId") .HasColumnType("uniqueidentifier"); + b.Property("ResellPrice") + .HasColumnType("decimal(18,2)"); + b.Property("Seats") .HasColumnType("nvarchar(max)"); @@ -349,9 +352,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.HasOne("TickAPI.Customers.Models.Customer", "Owner") .WithMany("Tickets") - .HasForeignKey("OwnerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + .HasForeignKey("OwnerId"); b.HasOne("TickAPI.TicketTypes.Models.TicketType", "Type") .WithMany("Tickets") diff --git a/TickAPI/TickAPI/Tickets/Models/Ticket.cs b/TickAPI/TickAPI/Tickets/Models/Ticket.cs index fdd8770..3805153 100644 --- a/TickAPI/TickAPI/Tickets/Models/Ticket.cs +++ b/TickAPI/TickAPI/Tickets/Models/Ticket.cs @@ -7,7 +7,7 @@ public class Ticket { public Guid Id { get; set; } public TicketType Type { get; set; } - public Customer Owner { get; set; } + public Customer? Owner { get; set; } public string NameOnTicket { get; set; } public string? Seats { get; set; } public bool ForResell { get; set; } diff --git a/TickAPI/TickAPI/Tickets/Services/TicketService.cs b/TickAPI/TickAPI/Tickets/Services/TicketService.cs index b16e64e..21098da 100644 --- a/TickAPI/TickAPI/Tickets/Services/TicketService.cs +++ b/TickAPI/TickAPI/Tickets/Services/TicketService.cs @@ -123,7 +123,7 @@ public async Task SetTicketForResellAsync(Guid ticketId, string email, d return Result.PropagateError(ticketRes); } - if (ticketRes.Value!.Type.Price < 1.5m*resellPrice) + if (ticketRes.Value!.Type.Price*1.5m < resellPrice) { return Result.Failure(StatusCodes.Status500InternalServerError, "Resell price cannot exceed " + "value of original price times 1.5"); @@ -133,6 +133,11 @@ public async Task SetTicketForResellAsync(Guid ticketId, string email, d { return Result.Failure(StatusCodes.Status500InternalServerError, "Ticket is already set for resell"); } + + if (ticketRes.Value!.Used) + { + return Result.Failure(StatusCodes.Status500InternalServerError, "Ticket is already used"); + } var res = await _ticketRepository.SetTicketForResell(ticketId, resellPrice); return res; From 8f447bd8d079eaa8acbde4cf5bdb3af86d9923c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Sat, 24 May 2025 13:18:37 +0200 Subject: [PATCH 72/80] tests --- .../Tickets/Services/TicketServiceTests.cs | 101 +++++ ...0250524110838_nonnullableowenr.Designer.cs | 395 ++++++++++++++++++ .../20250524110838_nonnullableowenr.cs | 60 +++ .../TickApiDbContextModelSnapshot.cs | 6 +- TickAPI/TickAPI/Tickets/Models/Ticket.cs | 2 +- 5 files changed, 561 insertions(+), 3 deletions(-) create mode 100644 TickAPI/TickAPI/Migrations/20250524110838_nonnullableowenr.Designer.cs create mode 100644 TickAPI/TickAPI/Migrations/20250524110838_nonnullableowenr.cs diff --git a/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs b/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs index 331853f..aba720e 100644 --- a/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs @@ -590,4 +590,105 @@ public async Task ScanTicket_WhenScanningSuccesful_ShouldReturnSuccess() // Assert Assert.True(res.IsSuccess); } + + [Fact] + public async Task SetTicketForResellAsync_ReturnsFailure_WhenPriceIsZero() + { + var ticketRepositoryMock = new Mock(); + var paginationServiceMock = new Mock(); + var qrServiceMock = new Mock(); + var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + + var result = await sut.SetTicketForResellAsync(Guid.NewGuid(), "test@example.com", 0); + + Assert.True(result.IsError); + Assert.Equal(500, result.StatusCode); + } + + [Fact] + public async Task SetTicketForResellAsync_ReturnsFailure_WhenPriceTooHigh() + { + var ticket = CreateTicketForResellTest(price: 100); + var ticketRepositoryMock = new Mock(); + var paginationServiceMock = new Mock(); + var qrServiceMock = new Mock(); + ticketRepositoryMock + .Setup(r => r.GetTicketWithDetailsByIdAndEmailAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(Result.Success(ticket)); + + var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + var result = await sut.SetTicketForResellAsync(Guid.NewGuid(), "test@example.com", 160); + + Assert.True(result.IsError); + Assert.Equal(500, result.StatusCode); + } + + [Fact] + public async Task SetTicketForResellAsync_ReturnsFailure_WhenAlreadyForResell() + { + var ticket = CreateTicketForResellTest(price: 100, forResell: true); + var ticketRepositoryMock = new Mock(); + var paginationServiceMock = new Mock(); + var qrServiceMock = new Mock(); + ticketRepositoryMock + .Setup(r => r.GetTicketWithDetailsByIdAndEmailAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(Result.Success(ticket)); + var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + var result = await sut.SetTicketForResellAsync(Guid.NewGuid(), "test@example.com", 120); + + Assert.True(result.IsError); + Assert.Equal(500, result.StatusCode); + } + + [Fact] + public async Task SetTicketForResellAsync_ReturnsFailure_WhenTicketIsUsed() + { + var ticket = CreateTicketForResellTest(price: 100, used: true); + var ticketRepositoryMock = new Mock(); + var paginationServiceMock = new Mock(); + var qrServiceMock = new Mock(); + + ticketRepositoryMock + .Setup(r => r.GetTicketWithDetailsByIdAndEmailAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(Result.Success(ticket)); + var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + + var result = await sut.SetTicketForResellAsync(Guid.NewGuid(), "test@example.com", 120); + + Assert.True(result.IsError); + Assert.Equal(500, result.StatusCode); + } + + [Fact] + public async Task SetTicketForResellAsync_ReturnsSuccess_WhenValid() + { + var ticketRepositoryMock = new Mock(); + var paginationServiceMock = new Mock(); + var qrServiceMock = new Mock(); + var ticket = CreateTicketForResellTest(price: 100); + ticketRepositoryMock + .Setup(r => r.GetTicketWithDetailsByIdAndEmailAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(Result.Success(ticket)); + + ticketRepositoryMock + .Setup(r => r.SetTicketForResell(It.IsAny(), It.IsAny())) + .ReturnsAsync(Result.Success()); + + var sut = new TicketService(ticketRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); + + var result = await sut.SetTicketForResellAsync(Guid.NewGuid(), "test@example.com", 130); + + Assert.False(result.IsError); + } + + private Ticket CreateTicketForResellTest(decimal price, bool forResell = false, bool used = false) + { + return new Ticket + { + Id = Guid.NewGuid(), + Type = new TicketType { Price = price }, + ForResell = forResell, + Used = used + }; + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Migrations/20250524110838_nonnullableowenr.Designer.cs b/TickAPI/TickAPI/Migrations/20250524110838_nonnullableowenr.Designer.cs new file mode 100644 index 0000000..60a8d36 --- /dev/null +++ b/TickAPI/TickAPI/Migrations/20250524110838_nonnullableowenr.Designer.cs @@ -0,0 +1,395 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using TickAPI.Common.TickApiDbContext; + +#nullable disable + +namespace TickAPI.Migrations +{ + [DbContext(typeof(TickApiDbContext))] + [Migration("20250524110838_nonnullableowenr")] + partial class nonnullableowenr + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("CategoryEvent", b => + { + b.Property("CategoriesId") + .HasColumnType("uniqueidentifier"); + + b.Property("EventsId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("CategoriesId", "EventsId"); + + b.HasIndex("EventsId"); + + b.ToTable("CategoryEvent"); + }); + + modelBuilder.Entity("TickAPI.Addresses.Models.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("City") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Country") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FlatNumber") + .HasColumnType("bigint"); + + b.Property("HouseNumber") + .HasColumnType("bigint"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Street") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Addresses"); + }); + + modelBuilder.Entity("TickAPI.Admins.Models.Admin", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Login") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Admins"); + }); + + modelBuilder.Entity("TickAPI.Categories.Models.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + + b.HasData( + new + { + Id = new Guid("ec3daf69-baa9-4fcd-a674-c09884a57272"), + Name = "Music" + }, + new + { + Id = new Guid("de89dd76-3b29-43e1-8f4b-5278b1b8bde2"), + Name = "Sports" + }, + new + { + Id = new Guid("ea58370b-2a17-4770-abea-66399ad69fb8"), + Name = "Conferences" + }, + new + { + Id = new Guid("4a086d9e-59de-4fd1-a1b2-bd9b5eec797c"), + Name = "Theatre" + }, + new + { + Id = new Guid("5f8dbe65-30be-453f-8f22-191a11b2977b"), + Name = "Comedy" + }, + new + { + Id = new Guid("4421327a-4bc8-4706-bec0-666f78ed0c69"), + Name = "Workshops" + }); + }); + + modelBuilder.Entity("TickAPI.Customers.Models.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Event", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AddressId") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("EventStatus") + .HasColumnType("int"); + + b.Property("MinimumAge") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OrganizerId") + .HasColumnType("uniqueidentifier"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("AddressId"); + + b.HasIndex("OrganizerId"); + + b.ToTable("Events"); + }); + + modelBuilder.Entity("TickAPI.Organizers.Models.Organizer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsVerified") + .HasColumnType("bit"); + + b.Property("LastName") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Organizers"); + }); + + modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AvailableFrom") + .HasColumnType("datetime2"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EventId") + .HasColumnType("uniqueidentifier"); + + b.Property("MaxCount") + .HasColumnType("bigint"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("EventId"); + + b.ToTable("TicketTypes"); + }); + + modelBuilder.Entity("TickAPI.Tickets.Models.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ForResell") + .HasColumnType("bit"); + + b.Property("NameOnTicket") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("uniqueidentifier"); + + b.Property("ResellPrice") + .HasColumnType("decimal(18,2)"); + + b.Property("Seats") + .HasColumnType("nvarchar(max)"); + + b.Property("TypeId") + .HasColumnType("uniqueidentifier"); + + b.Property("Used") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("TypeId"); + + b.ToTable("Tickets"); + }); + + modelBuilder.Entity("CategoryEvent", b => + { + b.HasOne("TickAPI.Categories.Models.Category", null) + .WithMany() + .HasForeignKey("CategoriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TickAPI.Events.Models.Event", null) + .WithMany() + .HasForeignKey("EventsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Event", b => + { + b.HasOne("TickAPI.Addresses.Models.Address", "Address") + .WithMany() + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TickAPI.Organizers.Models.Organizer", "Organizer") + .WithMany("Events") + .HasForeignKey("OrganizerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Address"); + + b.Navigation("Organizer"); + }); + + modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => + { + b.HasOne("TickAPI.Events.Models.Event", "Event") + .WithMany("TicketTypes") + .HasForeignKey("EventId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Event"); + }); + + modelBuilder.Entity("TickAPI.Tickets.Models.Ticket", b => + { + b.HasOne("TickAPI.Customers.Models.Customer", "Owner") + .WithMany("Tickets") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TickAPI.TicketTypes.Models.TicketType", "Type") + .WithMany("Tickets") + .HasForeignKey("TypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + + b.Navigation("Type"); + }); + + modelBuilder.Entity("TickAPI.Customers.Models.Customer", b => + { + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Event", b => + { + b.Navigation("TicketTypes"); + }); + + modelBuilder.Entity("TickAPI.Organizers.Models.Organizer", b => + { + b.Navigation("Events"); + }); + + modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => + { + b.Navigation("Tickets"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TickAPI/TickAPI/Migrations/20250524110838_nonnullableowenr.cs b/TickAPI/TickAPI/Migrations/20250524110838_nonnullableowenr.cs new file mode 100644 index 0000000..b7e500b --- /dev/null +++ b/TickAPI/TickAPI/Migrations/20250524110838_nonnullableowenr.cs @@ -0,0 +1,60 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TickAPI.Migrations +{ + /// + public partial class nonnullableowenr : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Tickets_Customers_OwnerId", + table: "Tickets"); + + migrationBuilder.AlterColumn( + name: "OwnerId", + table: "Tickets", + type: "uniqueidentifier", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), + oldClrType: typeof(Guid), + oldType: "uniqueidentifier", + oldNullable: true); + + migrationBuilder.AddForeignKey( + name: "FK_Tickets_Customers_OwnerId", + table: "Tickets", + column: "OwnerId", + principalTable: "Customers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Tickets_Customers_OwnerId", + table: "Tickets"); + + migrationBuilder.AlterColumn( + name: "OwnerId", + table: "Tickets", + type: "uniqueidentifier", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uniqueidentifier"); + + migrationBuilder.AddForeignKey( + name: "FK_Tickets_Customers_OwnerId", + table: "Tickets", + column: "OwnerId", + principalTable: "Customers", + principalColumn: "Id"); + } + } +} diff --git a/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs b/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs index c6fa5bc..d0dfd47 100644 --- a/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs +++ b/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs @@ -279,7 +279,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("nvarchar(max)"); - b.Property("OwnerId") + b.Property("OwnerId") .HasColumnType("uniqueidentifier"); b.Property("ResellPrice") @@ -352,7 +352,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.HasOne("TickAPI.Customers.Models.Customer", "Owner") .WithMany("Tickets") - .HasForeignKey("OwnerId"); + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); b.HasOne("TickAPI.TicketTypes.Models.TicketType", "Type") .WithMany("Tickets") diff --git a/TickAPI/TickAPI/Tickets/Models/Ticket.cs b/TickAPI/TickAPI/Tickets/Models/Ticket.cs index 3805153..fdd8770 100644 --- a/TickAPI/TickAPI/Tickets/Models/Ticket.cs +++ b/TickAPI/TickAPI/Tickets/Models/Ticket.cs @@ -7,7 +7,7 @@ public class Ticket { public Guid Id { get; set; } public TicketType Type { get; set; } - public Customer? Owner { get; set; } + public Customer Owner { get; set; } public string NameOnTicket { get; set; } public string? Seats { get; set; } public bool ForResell { get; set; } From aec86da3a2289c12643420a1c93f60752f4a5b78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Sat, 24 May 2025 13:23:49 +0200 Subject: [PATCH 73/80] rever miratuons --- .../20250524105603_nullableowner.Designer.cs | 393 ----------------- .../20250524105603_nullableowner.cs | 60 --- ...0250524110838_nonnullableowenr.Designer.cs | 395 ------------------ .../20250524110838_nonnullableowenr.cs | 60 --- .../TickApiDbContextModelSnapshot.cs | 18 +- 5 files changed, 9 insertions(+), 917 deletions(-) delete mode 100644 TickAPI/TickAPI/Migrations/20250524105603_nullableowner.Designer.cs delete mode 100644 TickAPI/TickAPI/Migrations/20250524105603_nullableowner.cs delete mode 100644 TickAPI/TickAPI/Migrations/20250524110838_nonnullableowenr.Designer.cs delete mode 100644 TickAPI/TickAPI/Migrations/20250524110838_nonnullableowenr.cs diff --git a/TickAPI/TickAPI/Migrations/20250524105603_nullableowner.Designer.cs b/TickAPI/TickAPI/Migrations/20250524105603_nullableowner.Designer.cs deleted file mode 100644 index 5d1fbea..0000000 --- a/TickAPI/TickAPI/Migrations/20250524105603_nullableowner.Designer.cs +++ /dev/null @@ -1,393 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using TickAPI.Common.TickApiDbContext; - -#nullable disable - -namespace TickAPI.Migrations -{ - [DbContext(typeof(TickApiDbContext))] - [Migration("20250524105603_nullableowner")] - partial class nullableowner - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "9.0.2") - .HasAnnotation("Relational:MaxIdentifierLength", 128); - - SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); - - modelBuilder.Entity("CategoryEvent", b => - { - b.Property("CategoriesId") - .HasColumnType("uniqueidentifier"); - - b.Property("EventsId") - .HasColumnType("uniqueidentifier"); - - b.HasKey("CategoriesId", "EventsId"); - - b.HasIndex("EventsId"); - - b.ToTable("CategoryEvent"); - }); - - modelBuilder.Entity("TickAPI.Addresses.Models.Address", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier"); - - b.Property("City") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("Country") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("FlatNumber") - .HasColumnType("bigint"); - - b.Property("HouseNumber") - .HasColumnType("bigint"); - - b.Property("PostalCode") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("Street") - .HasColumnType("nvarchar(max)"); - - b.HasKey("Id"); - - b.ToTable("Addresses"); - }); - - modelBuilder.Entity("TickAPI.Admins.Models.Admin", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier"); - - b.Property("Email") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("Login") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.HasKey("Id"); - - b.ToTable("Admins"); - }); - - modelBuilder.Entity("TickAPI.Categories.Models.Category", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier"); - - b.Property("Name") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.HasKey("Id"); - - b.ToTable("Categories"); - - b.HasData( - new - { - Id = new Guid("ec3daf69-baa9-4fcd-a674-c09884a57272"), - Name = "Music" - }, - new - { - Id = new Guid("de89dd76-3b29-43e1-8f4b-5278b1b8bde2"), - Name = "Sports" - }, - new - { - Id = new Guid("ea58370b-2a17-4770-abea-66399ad69fb8"), - Name = "Conferences" - }, - new - { - Id = new Guid("4a086d9e-59de-4fd1-a1b2-bd9b5eec797c"), - Name = "Theatre" - }, - new - { - Id = new Guid("5f8dbe65-30be-453f-8f22-191a11b2977b"), - Name = "Comedy" - }, - new - { - Id = new Guid("4421327a-4bc8-4706-bec0-666f78ed0c69"), - Name = "Workshops" - }); - }); - - modelBuilder.Entity("TickAPI.Customers.Models.Customer", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier"); - - b.Property("CreationDate") - .HasColumnType("datetime2"); - - b.Property("Email") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("FirstName") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("LastName") - .HasColumnType("nvarchar(max)"); - - b.HasKey("Id"); - - b.ToTable("Customers"); - }); - - modelBuilder.Entity("TickAPI.Events.Models.Event", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier"); - - b.Property("AddressId") - .HasColumnType("uniqueidentifier"); - - b.Property("Description") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("EndDate") - .HasColumnType("datetime2"); - - b.Property("EventStatus") - .HasColumnType("int"); - - b.Property("MinimumAge") - .HasColumnType("bigint"); - - b.Property("Name") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("OrganizerId") - .HasColumnType("uniqueidentifier"); - - b.Property("StartDate") - .HasColumnType("datetime2"); - - b.HasKey("Id"); - - b.HasIndex("AddressId"); - - b.HasIndex("OrganizerId"); - - b.ToTable("Events"); - }); - - modelBuilder.Entity("TickAPI.Organizers.Models.Organizer", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier"); - - b.Property("CreationDate") - .HasColumnType("datetime2"); - - b.Property("DisplayName") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("Email") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("FirstName") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("IsVerified") - .HasColumnType("bit"); - - b.Property("LastName") - .HasColumnType("nvarchar(max)"); - - b.HasKey("Id"); - - b.ToTable("Organizers"); - }); - - modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier"); - - b.Property("AvailableFrom") - .HasColumnType("datetime2"); - - b.Property("Currency") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("Description") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("EventId") - .HasColumnType("uniqueidentifier"); - - b.Property("MaxCount") - .HasColumnType("bigint"); - - b.Property("Price") - .HasColumnType("decimal(18,2)"); - - b.HasKey("Id"); - - b.HasIndex("EventId"); - - b.ToTable("TicketTypes"); - }); - - modelBuilder.Entity("TickAPI.Tickets.Models.Ticket", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier"); - - b.Property("ForResell") - .HasColumnType("bit"); - - b.Property("NameOnTicket") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("OwnerId") - .HasColumnType("uniqueidentifier"); - - b.Property("ResellPrice") - .HasColumnType("decimal(18,2)"); - - b.Property("Seats") - .HasColumnType("nvarchar(max)"); - - b.Property("TypeId") - .HasColumnType("uniqueidentifier"); - - b.Property("Used") - .HasColumnType("bit"); - - b.HasKey("Id"); - - b.HasIndex("OwnerId"); - - b.HasIndex("TypeId"); - - b.ToTable("Tickets"); - }); - - modelBuilder.Entity("CategoryEvent", b => - { - b.HasOne("TickAPI.Categories.Models.Category", null) - .WithMany() - .HasForeignKey("CategoriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("TickAPI.Events.Models.Event", null) - .WithMany() - .HasForeignKey("EventsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("TickAPI.Events.Models.Event", b => - { - b.HasOne("TickAPI.Addresses.Models.Address", "Address") - .WithMany() - .HasForeignKey("AddressId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("TickAPI.Organizers.Models.Organizer", "Organizer") - .WithMany("Events") - .HasForeignKey("OrganizerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Address"); - - b.Navigation("Organizer"); - }); - - modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => - { - b.HasOne("TickAPI.Events.Models.Event", "Event") - .WithMany("TicketTypes") - .HasForeignKey("EventId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Event"); - }); - - modelBuilder.Entity("TickAPI.Tickets.Models.Ticket", b => - { - b.HasOne("TickAPI.Customers.Models.Customer", "Owner") - .WithMany("Tickets") - .HasForeignKey("OwnerId"); - - b.HasOne("TickAPI.TicketTypes.Models.TicketType", "Type") - .WithMany("Tickets") - .HasForeignKey("TypeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Owner"); - - b.Navigation("Type"); - }); - - modelBuilder.Entity("TickAPI.Customers.Models.Customer", b => - { - b.Navigation("Tickets"); - }); - - modelBuilder.Entity("TickAPI.Events.Models.Event", b => - { - b.Navigation("TicketTypes"); - }); - - modelBuilder.Entity("TickAPI.Organizers.Models.Organizer", b => - { - b.Navigation("Events"); - }); - - modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => - { - b.Navigation("Tickets"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/TickAPI/TickAPI/Migrations/20250524105603_nullableowner.cs b/TickAPI/TickAPI/Migrations/20250524105603_nullableowner.cs deleted file mode 100644 index b868a03..0000000 --- a/TickAPI/TickAPI/Migrations/20250524105603_nullableowner.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace TickAPI.Migrations -{ - /// - public partial class nullableowner : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropForeignKey( - name: "FK_Tickets_Customers_OwnerId", - table: "Tickets"); - - migrationBuilder.AlterColumn( - name: "OwnerId", - table: "Tickets", - type: "uniqueidentifier", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uniqueidentifier"); - - migrationBuilder.AddForeignKey( - name: "FK_Tickets_Customers_OwnerId", - table: "Tickets", - column: "OwnerId", - principalTable: "Customers", - principalColumn: "Id"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropForeignKey( - name: "FK_Tickets_Customers_OwnerId", - table: "Tickets"); - - migrationBuilder.AlterColumn( - name: "OwnerId", - table: "Tickets", - type: "uniqueidentifier", - nullable: false, - defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), - oldClrType: typeof(Guid), - oldType: "uniqueidentifier", - oldNullable: true); - - migrationBuilder.AddForeignKey( - name: "FK_Tickets_Customers_OwnerId", - table: "Tickets", - column: "OwnerId", - principalTable: "Customers", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - } - } -} diff --git a/TickAPI/TickAPI/Migrations/20250524110838_nonnullableowenr.Designer.cs b/TickAPI/TickAPI/Migrations/20250524110838_nonnullableowenr.Designer.cs deleted file mode 100644 index 60a8d36..0000000 --- a/TickAPI/TickAPI/Migrations/20250524110838_nonnullableowenr.Designer.cs +++ /dev/null @@ -1,395 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using TickAPI.Common.TickApiDbContext; - -#nullable disable - -namespace TickAPI.Migrations -{ - [DbContext(typeof(TickApiDbContext))] - [Migration("20250524110838_nonnullableowenr")] - partial class nonnullableowenr - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "9.0.2") - .HasAnnotation("Relational:MaxIdentifierLength", 128); - - SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); - - modelBuilder.Entity("CategoryEvent", b => - { - b.Property("CategoriesId") - .HasColumnType("uniqueidentifier"); - - b.Property("EventsId") - .HasColumnType("uniqueidentifier"); - - b.HasKey("CategoriesId", "EventsId"); - - b.HasIndex("EventsId"); - - b.ToTable("CategoryEvent"); - }); - - modelBuilder.Entity("TickAPI.Addresses.Models.Address", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier"); - - b.Property("City") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("Country") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("FlatNumber") - .HasColumnType("bigint"); - - b.Property("HouseNumber") - .HasColumnType("bigint"); - - b.Property("PostalCode") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("Street") - .HasColumnType("nvarchar(max)"); - - b.HasKey("Id"); - - b.ToTable("Addresses"); - }); - - modelBuilder.Entity("TickAPI.Admins.Models.Admin", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier"); - - b.Property("Email") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("Login") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.HasKey("Id"); - - b.ToTable("Admins"); - }); - - modelBuilder.Entity("TickAPI.Categories.Models.Category", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier"); - - b.Property("Name") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.HasKey("Id"); - - b.ToTable("Categories"); - - b.HasData( - new - { - Id = new Guid("ec3daf69-baa9-4fcd-a674-c09884a57272"), - Name = "Music" - }, - new - { - Id = new Guid("de89dd76-3b29-43e1-8f4b-5278b1b8bde2"), - Name = "Sports" - }, - new - { - Id = new Guid("ea58370b-2a17-4770-abea-66399ad69fb8"), - Name = "Conferences" - }, - new - { - Id = new Guid("4a086d9e-59de-4fd1-a1b2-bd9b5eec797c"), - Name = "Theatre" - }, - new - { - Id = new Guid("5f8dbe65-30be-453f-8f22-191a11b2977b"), - Name = "Comedy" - }, - new - { - Id = new Guid("4421327a-4bc8-4706-bec0-666f78ed0c69"), - Name = "Workshops" - }); - }); - - modelBuilder.Entity("TickAPI.Customers.Models.Customer", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier"); - - b.Property("CreationDate") - .HasColumnType("datetime2"); - - b.Property("Email") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("FirstName") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("LastName") - .HasColumnType("nvarchar(max)"); - - b.HasKey("Id"); - - b.ToTable("Customers"); - }); - - modelBuilder.Entity("TickAPI.Events.Models.Event", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier"); - - b.Property("AddressId") - .HasColumnType("uniqueidentifier"); - - b.Property("Description") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("EndDate") - .HasColumnType("datetime2"); - - b.Property("EventStatus") - .HasColumnType("int"); - - b.Property("MinimumAge") - .HasColumnType("bigint"); - - b.Property("Name") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("OrganizerId") - .HasColumnType("uniqueidentifier"); - - b.Property("StartDate") - .HasColumnType("datetime2"); - - b.HasKey("Id"); - - b.HasIndex("AddressId"); - - b.HasIndex("OrganizerId"); - - b.ToTable("Events"); - }); - - modelBuilder.Entity("TickAPI.Organizers.Models.Organizer", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier"); - - b.Property("CreationDate") - .HasColumnType("datetime2"); - - b.Property("DisplayName") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("Email") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("FirstName") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("IsVerified") - .HasColumnType("bit"); - - b.Property("LastName") - .HasColumnType("nvarchar(max)"); - - b.HasKey("Id"); - - b.ToTable("Organizers"); - }); - - modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier"); - - b.Property("AvailableFrom") - .HasColumnType("datetime2"); - - b.Property("Currency") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("Description") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("EventId") - .HasColumnType("uniqueidentifier"); - - b.Property("MaxCount") - .HasColumnType("bigint"); - - b.Property("Price") - .HasColumnType("decimal(18,2)"); - - b.HasKey("Id"); - - b.HasIndex("EventId"); - - b.ToTable("TicketTypes"); - }); - - modelBuilder.Entity("TickAPI.Tickets.Models.Ticket", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier"); - - b.Property("ForResell") - .HasColumnType("bit"); - - b.Property("NameOnTicket") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("OwnerId") - .HasColumnType("uniqueidentifier"); - - b.Property("ResellPrice") - .HasColumnType("decimal(18,2)"); - - b.Property("Seats") - .HasColumnType("nvarchar(max)"); - - b.Property("TypeId") - .HasColumnType("uniqueidentifier"); - - b.Property("Used") - .HasColumnType("bit"); - - b.HasKey("Id"); - - b.HasIndex("OwnerId"); - - b.HasIndex("TypeId"); - - b.ToTable("Tickets"); - }); - - modelBuilder.Entity("CategoryEvent", b => - { - b.HasOne("TickAPI.Categories.Models.Category", null) - .WithMany() - .HasForeignKey("CategoriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("TickAPI.Events.Models.Event", null) - .WithMany() - .HasForeignKey("EventsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("TickAPI.Events.Models.Event", b => - { - b.HasOne("TickAPI.Addresses.Models.Address", "Address") - .WithMany() - .HasForeignKey("AddressId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("TickAPI.Organizers.Models.Organizer", "Organizer") - .WithMany("Events") - .HasForeignKey("OrganizerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Address"); - - b.Navigation("Organizer"); - }); - - modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => - { - b.HasOne("TickAPI.Events.Models.Event", "Event") - .WithMany("TicketTypes") - .HasForeignKey("EventId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Event"); - }); - - modelBuilder.Entity("TickAPI.Tickets.Models.Ticket", b => - { - b.HasOne("TickAPI.Customers.Models.Customer", "Owner") - .WithMany("Tickets") - .HasForeignKey("OwnerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("TickAPI.TicketTypes.Models.TicketType", "Type") - .WithMany("Tickets") - .HasForeignKey("TypeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Owner"); - - b.Navigation("Type"); - }); - - modelBuilder.Entity("TickAPI.Customers.Models.Customer", b => - { - b.Navigation("Tickets"); - }); - - modelBuilder.Entity("TickAPI.Events.Models.Event", b => - { - b.Navigation("TicketTypes"); - }); - - modelBuilder.Entity("TickAPI.Organizers.Models.Organizer", b => - { - b.Navigation("Events"); - }); - - modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => - { - b.Navigation("Tickets"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/TickAPI/TickAPI/Migrations/20250524110838_nonnullableowenr.cs b/TickAPI/TickAPI/Migrations/20250524110838_nonnullableowenr.cs deleted file mode 100644 index b7e500b..0000000 --- a/TickAPI/TickAPI/Migrations/20250524110838_nonnullableowenr.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace TickAPI.Migrations -{ - /// - public partial class nonnullableowenr : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropForeignKey( - name: "FK_Tickets_Customers_OwnerId", - table: "Tickets"); - - migrationBuilder.AlterColumn( - name: "OwnerId", - table: "Tickets", - type: "uniqueidentifier", - nullable: false, - defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), - oldClrType: typeof(Guid), - oldType: "uniqueidentifier", - oldNullable: true); - - migrationBuilder.AddForeignKey( - name: "FK_Tickets_Customers_OwnerId", - table: "Tickets", - column: "OwnerId", - principalTable: "Customers", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropForeignKey( - name: "FK_Tickets_Customers_OwnerId", - table: "Tickets"); - - migrationBuilder.AlterColumn( - name: "OwnerId", - table: "Tickets", - type: "uniqueidentifier", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uniqueidentifier"); - - migrationBuilder.AddForeignKey( - name: "FK_Tickets_Customers_OwnerId", - table: "Tickets", - column: "OwnerId", - principalTable: "Customers", - principalColumn: "Id"); - } - } -} diff --git a/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs b/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs index d0dfd47..819b0aa 100644 --- a/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs +++ b/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs @@ -34,7 +34,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("EventsId"); - b.ToTable("CategoryEvent"); + b.ToTable("CategoryEvent", (string)null); }); modelBuilder.Entity("TickAPI.Addresses.Models.Address", b => @@ -66,7 +66,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.ToTable("Addresses"); + b.ToTable("Addresses", (string)null); }); modelBuilder.Entity("TickAPI.Admins.Models.Admin", b => @@ -85,7 +85,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.ToTable("Admins"); + b.ToTable("Admins", (string)null); }); modelBuilder.Entity("TickAPI.Categories.Models.Category", b => @@ -100,7 +100,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.ToTable("Categories"); + b.ToTable("Categories", (string)null); b.HasData( new @@ -157,7 +157,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.ToTable("Customers"); + b.ToTable("Customers", (string)null); }); modelBuilder.Entity("TickAPI.Events.Models.Event", b => @@ -198,7 +198,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("OrganizerId"); - b.ToTable("Events"); + b.ToTable("Events", (string)null); }); modelBuilder.Entity("TickAPI.Organizers.Models.Organizer", b => @@ -230,7 +230,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.ToTable("Organizers"); + b.ToTable("Organizers", (string)null); }); modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => @@ -263,7 +263,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("EventId"); - b.ToTable("TicketTypes"); + b.ToTable("TicketTypes", (string)null); }); modelBuilder.Entity("TickAPI.Tickets.Models.Ticket", b => @@ -300,7 +300,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("TypeId"); - b.ToTable("Tickets"); + b.ToTable("Tickets", (string)null); }); modelBuilder.Entity("CategoryEvent", b => From 1bccf0c71e7dec372cbb0b39f7e535967469fe64 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Sat, 24 May 2025 21:51:57 +0200 Subject: [PATCH 74/80] changed ToObjectResult in Result.cs to return 200 on success instead of 204 --- TickAPI/TickAPI/Common/Results/Result.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TickAPI/TickAPI/Common/Results/Result.cs b/TickAPI/TickAPI/Common/Results/Result.cs index 2e7f4ec..8d850d7 100644 --- a/TickAPI/TickAPI/Common/Results/Result.cs +++ b/TickAPI/TickAPI/Common/Results/Result.cs @@ -57,7 +57,7 @@ public ObjectResult ToObjectResult(int successCode = StatusCodes.Status200OK) }; } - return new ObjectResult(null) + return new ObjectResult(string.Empty) { StatusCode = successCode }; From b649e813f6b4a6708dd9a4f43067178dae5be225 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Sat, 24 May 2025 21:56:35 +0200 Subject: [PATCH 75/80] ShoppingCartsController.cs uses ToObjectResult --- .../Controllers/ShoppingCartsController.cs | 53 ++++++------------- .../DTOs/Response/CheckoutResponseDto.cs | 6 --- 2 files changed, 15 insertions(+), 44 deletions(-) delete mode 100644 TickAPI/TickAPI/ShoppingCarts/DTOs/Response/CheckoutResponseDto.cs diff --git a/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs b/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs index 1780d8d..373d939 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Controllers/ShoppingCartsController.cs @@ -2,6 +2,7 @@ using TickAPI.Common.Auth.Attributes; using TickAPI.Common.Auth.Enums; using TickAPI.Common.Claims.Abstractions; +using TickAPI.Common.Payment.Models; using TickAPI.ShoppingCarts.Abstractions; using TickAPI.ShoppingCarts.DTOs.Request; using TickAPI.ShoppingCarts.DTOs.Response; @@ -28,19 +29,15 @@ public async Task AddTickets([FromBody] AddNewTicketDto addTicketD var emailResult = _claimsService.GetEmailFromClaims(User.Claims); if (emailResult.IsError) { - return StatusCode(emailResult.StatusCode, emailResult.ErrorMsg); + return emailResult.ToObjectResult(); } var email = emailResult.Value!; var addTicketResult = await _shoppingCartService.AddNewTicketsToCartAsync(addTicketDto.TicketTypeId, addTicketDto.Amount, email); - if (addTicketResult.IsError) - { - return StatusCode(addTicketResult.StatusCode, addTicketResult.ErrorMsg); - } - - return Ok(); + + return addTicketResult.ToObjectResult(); } [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] @@ -50,17 +47,13 @@ public async Task> GetTickets() var emailResult = _claimsService.GetEmailFromClaims(User.Claims); if (emailResult.IsError) { - return StatusCode(emailResult.StatusCode, emailResult.ErrorMsg); + return emailResult.ToObjectResult(); } var email = emailResult.Value!; var getTicketsResult = await _shoppingCartService.GetTicketsFromCartAsync(email); - if (getTicketsResult.IsError) - { - return StatusCode(getTicketsResult.StatusCode, getTicketsResult.ErrorMsg); - } - - return Ok(getTicketsResult.Value); + + return getTicketsResult.ToObjectResult(); } [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] @@ -70,19 +63,15 @@ public async Task RemoveTickets([FromBody] RemoveNewTicketDto remo var emailResult = _claimsService.GetEmailFromClaims(User.Claims); if (emailResult.IsError) { - return StatusCode(emailResult.StatusCode, emailResult.ErrorMsg); + return emailResult.ToObjectResult(); } var email = emailResult.Value!; - var addTicketResult = + var removeTicketResult = await _shoppingCartService.RemoveNewTicketsFromCartAsync(removeTicketDto.TicketTypeId, removeTicketDto.Amount, email); - if (addTicketResult.IsError) - { - return StatusCode(addTicketResult.StatusCode, addTicketResult.ErrorMsg); - } - return Ok(); + return removeTicketResult.ToObjectResult(); } [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] @@ -92,41 +81,29 @@ public async Task>> GetDueAmount() var emailResult = _claimsService.GetEmailFromClaims(User.Claims); if (emailResult.IsError) { - return StatusCode(emailResult.StatusCode, emailResult.ErrorMsg); + return emailResult.ToObjectResult(); } var email = emailResult.Value!; var dueAmountResult = await _shoppingCartService.GetDueAmountAsync(email); - if (dueAmountResult.IsError) - { - return StatusCode(dueAmountResult.StatusCode, dueAmountResult.ErrorMsg); - } - - return Ok(dueAmountResult.Value); + return dueAmountResult.ToObjectResult(); } [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] [HttpPost("checkout")] - public async Task> Checkout([FromBody] CheckoutDto checkoutDto) + public async Task> Checkout([FromBody] CheckoutDto checkoutDto) { var emailResult = _claimsService.GetEmailFromClaims(User.Claims); if (emailResult.IsError) { - return StatusCode(emailResult.StatusCode, emailResult.ErrorMsg); + return emailResult.ToObjectResult(); } var email = emailResult.Value!; var checkoutResult = await _shoppingCartService.CheckoutAsync(email, checkoutDto.Amount, checkoutDto.Currency, checkoutDto.CardNumber, checkoutDto.CardExpiry, checkoutDto.Cvv); - if (checkoutResult.IsError) - { - return StatusCode(checkoutResult.StatusCode, checkoutResult.ErrorMsg); - } - - var checkout = checkoutResult.Value!; - - return Ok(new CheckoutResponseDto(checkout.TransactionId, checkout.Status)); + return checkoutResult.ToObjectResult(); } } \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/CheckoutResponseDto.cs b/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/CheckoutResponseDto.cs deleted file mode 100644 index 6a4b4f2..0000000 --- a/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/CheckoutResponseDto.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace TickAPI.ShoppingCarts.DTOs.Response; - -public record CheckoutResponseDto( - string TransactionId, - string Status -); \ No newline at end of file From f88d48715aaa7d61124fc35dc93d8db5c6eb3bf3 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Sat, 24 May 2025 21:59:51 +0200 Subject: [PATCH 76/80] stylistic fixes --- .../ShoppingCarts/Repositories/ShoppingCartRepository.cs | 4 ++-- TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs | 1 - TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs | 1 - TickAPI/TickAPI/Tickets/Services/TicketService.cs | 1 - 4 files changed, 2 insertions(+), 5 deletions(-) diff --git a/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs b/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs index a487c63..e4f7845 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs @@ -57,7 +57,7 @@ public async Task UpdateShoppingCartAsync(string customerEmail, Shopping public async Task AddNewTicketsToCartAsync(string customerEmail, Guid ticketTypeId, uint amount) { - if (amount <= 0) + if (amount == 0) { return Result.Failure(StatusCodes.Status400BadRequest, "amount of bought tickets must be greater than 0"); } @@ -105,7 +105,7 @@ public async Task AddNewTicketsToCartAsync(string customerEmail, Guid ti public async Task RemoveNewTicketsFromCartAsync(string customerEmail, Guid ticketTypeId, uint amount) { - if (amount <= 0) + if (amount == 0) { return Result.Failure(StatusCodes.Status400BadRequest, "amount of removed tickets must be greater than 0"); } diff --git a/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs b/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs index 877b9cd..ff20841 100644 --- a/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs +++ b/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs @@ -1,6 +1,5 @@ using TickAPI.Common.Results; using TickAPI.Common.Results.Generic; -using TickAPI.Customers.Models; using TickAPI.Tickets.Models; using TickAPI.TicketTypes.Models; diff --git a/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs b/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs index 303a4ab..d1fb208 100644 --- a/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs +++ b/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs @@ -21,7 +21,6 @@ public Task>> GetTicketsForCustome public Task> GetTicketDetailsAsync(Guid ticketGuid, string email, string scanUrl); public Task> GetTicketTypeByIdAsync(Guid ticketTypeId); - public Task CreateTicketAsync(TicketType type, Customer owner, string? nameOnTicket = null, string? seats = null); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Services/TicketService.cs b/TickAPI/TickAPI/Tickets/Services/TicketService.cs index 5d1cd5e..74b6dad 100644 --- a/TickAPI/TickAPI/Tickets/Services/TicketService.cs +++ b/TickAPI/TickAPI/Tickets/Services/TicketService.cs @@ -35,7 +35,6 @@ public TicketService(ITicketRepository ticketRepository, ITicketTypeRepository t _qrCodeService = qrCodeService; } - // TODO: Update this method to also count tickets cached in Redis as unavailable public async Task> GetNumberOfAvailableTicketsByTypeAsync(TicketType ticketType) { var unavailableTickets = _ticketRepository.GetAllTicketsByTicketType(ticketType); From e1efea5aaa6d9428b613d070db432f17691c09db Mon Sep 17 00:00:00 2001 From: kubapoke Date: Sat, 24 May 2025 22:00:34 +0200 Subject: [PATCH 77/80] added returning currency when getting cart items --- .../GetShoppingCartTicketsNewTicketDetailsResponseDto.cs | 3 ++- .../GetShoppingCartTicketsResellTicketDetailsResponseDto.cs | 3 ++- TickAPI/TickAPI/ShoppingCarts/Mappers/ShoppingCartMapper.cs | 6 ++++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsNewTicketDetailsResponseDto.cs b/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsNewTicketDetailsResponseDto.cs index f85e035..579a865 100644 --- a/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsNewTicketDetailsResponseDto.cs +++ b/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsNewTicketDetailsResponseDto.cs @@ -6,5 +6,6 @@ public record GetShoppingCartTicketsNewTicketDetailsResponseDto( string TicketType, string OrganizerName, uint Quantity, - decimal UnitPrice + decimal UnitPrice, + string Currency ); \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsResellTicketDetailsResponseDto.cs b/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsResellTicketDetailsResponseDto.cs index f2aa4d0..a56fc13 100644 --- a/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsResellTicketDetailsResponseDto.cs +++ b/TickAPI/TickAPI/ShoppingCarts/DTOs/Response/GetShoppingCartTicketsResellTicketDetailsResponseDto.cs @@ -6,5 +6,6 @@ public record GetShoppingCartTicketsResellTicketDetailsResponseDto( string TicketType, string OrganizerName, string OriginalOwnerEmail, - decimal Price + decimal Price, + string Currency ); \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Mappers/ShoppingCartMapper.cs b/TickAPI/TickAPI/ShoppingCarts/Mappers/ShoppingCartMapper.cs index ae3d653..9e75a25 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Mappers/ShoppingCartMapper.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Mappers/ShoppingCartMapper.cs @@ -15,7 +15,8 @@ public static GetShoppingCartTicketsNewTicketDetailsResponseDto type.Description, type.Event.Organizer.DisplayName, quantity, - type.Price + type.Price, + type.Currency ); } @@ -28,7 +29,8 @@ public static GetShoppingCartTicketsResellTicketDetailsResponseDto ticket.Type.Description, ticket.Type.Event.Organizer.DisplayName, ticket.Owner.Email, - ticket.Type.Price + ticket.Type.Price, + ticket.Type.Currency ); } } \ No newline at end of file From e8e3acc020c435b17898235c695beed0773940c3 Mon Sep 17 00:00:00 2001 From: kubapoke Date: Sun, 25 May 2025 01:28:46 +0200 Subject: [PATCH 78/80] program reads shopping cart lifetime and syncing interval from appsettings --- TickAPI/TickAPI/Program.cs | 3 +++ .../Background/ShoppingCartSyncBackgroundService.cs | 12 ++++++++---- .../ShoppingCarts/Options/ShoppingCartOptions.cs | 7 +++++++ .../Repositories/ShoppingCartRepository.cs | 13 ++++++++----- TickAPI/TickAPI/appsettings.example.json | 4 ++++ 5 files changed, 30 insertions(+), 9 deletions(-) create mode 100644 TickAPI/TickAPI/ShoppingCarts/Options/ShoppingCartOptions.cs diff --git a/TickAPI/TickAPI/Program.cs b/TickAPI/TickAPI/Program.cs index 17ed802..53441b3 100644 --- a/TickAPI/TickAPI/Program.cs +++ b/TickAPI/TickAPI/Program.cs @@ -46,6 +46,7 @@ using TickAPI.Common.QR.Services; using TickAPI.ShoppingCarts.Abstractions; using TickAPI.ShoppingCarts.Background; +using TickAPI.ShoppingCarts.Options; using TickAPI.ShoppingCarts.Repositories; using TickAPI.ShoppingCarts.Services; using TickAPI.TicketTypes.Abstractions; @@ -128,6 +129,8 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddHostedService(); +builder.Services.Configure( + builder.Configuration.GetSection("ShoppingCart")); // Add ticket type services builder.Services.AddScoped(); diff --git a/TickAPI/TickAPI/ShoppingCarts/Background/ShoppingCartSyncBackgroundService.cs b/TickAPI/TickAPI/ShoppingCarts/Background/ShoppingCartSyncBackgroundService.cs index fd66407..0e99449 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Background/ShoppingCartSyncBackgroundService.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Background/ShoppingCartSyncBackgroundService.cs @@ -1,5 +1,7 @@ -using TickAPI.Common.Redis.Abstractions; +using Microsoft.Extensions.Options; +using TickAPI.Common.Redis.Abstractions; using TickAPI.ShoppingCarts.Models; +using TickAPI.ShoppingCarts.Options; namespace TickAPI.ShoppingCarts.Background; @@ -7,12 +9,14 @@ public class ShoppingCartSyncBackgroundService : BackgroundService { private readonly IServiceProvider _serviceProvider; private readonly ILogger _logger; - private static readonly TimeSpan SyncInterval = TimeSpan.FromMinutes(5); + private readonly TimeSpan _syncInterval; - public ShoppingCartSyncBackgroundService(IServiceProvider serviceProvider, ILogger logger) + public ShoppingCartSyncBackgroundService(IServiceProvider serviceProvider, + ILogger logger, IOptions options) { _serviceProvider = serviceProvider; _logger = logger; + _syncInterval = TimeSpan.FromMinutes(options.Value.SyncIntervalMinutes); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) @@ -30,7 +34,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) _logger.LogError(ex, "Error while syncing shopping cart ticket counters"); } - await Task.Delay(SyncInterval, stoppingToken); + await Task.Delay(_syncInterval, stoppingToken); } } diff --git a/TickAPI/TickAPI/ShoppingCarts/Options/ShoppingCartOptions.cs b/TickAPI/TickAPI/ShoppingCarts/Options/ShoppingCartOptions.cs new file mode 100644 index 0000000..c2f03a2 --- /dev/null +++ b/TickAPI/TickAPI/ShoppingCarts/Options/ShoppingCartOptions.cs @@ -0,0 +1,7 @@ +namespace TickAPI.ShoppingCarts.Options; + +public class ShoppingCartOptions +{ + public int SyncIntervalMinutes { get; set; } + public int LifetimeMinutes { get; set; } +} \ No newline at end of file diff --git a/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs b/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs index e4f7845..493afa5 100644 --- a/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs +++ b/TickAPI/TickAPI/ShoppingCarts/Repositories/ShoppingCartRepository.cs @@ -1,8 +1,10 @@ -using TickAPI.Common.Redis.Abstractions; +using Microsoft.Extensions.Options; +using TickAPI.Common.Redis.Abstractions; using TickAPI.Common.Results; using TickAPI.Common.Results.Generic; using TickAPI.ShoppingCarts.Abstractions; using TickAPI.ShoppingCarts.Models; +using TickAPI.ShoppingCarts.Options; using TickAPI.Tickets.Models; namespace TickAPI.ShoppingCarts.Repositories; @@ -10,11 +12,12 @@ namespace TickAPI.ShoppingCarts.Repositories; public class ShoppingCartRepository : IShoppingCartRepository { private readonly IRedisService _redisService; - private static readonly TimeSpan DefaultExpiry = TimeSpan.FromMinutes(15); + private readonly TimeSpan _defaultExpiry; - public ShoppingCartRepository(IRedisService redisService) + public ShoppingCartRepository(IRedisService redisService, IOptions options) { _redisService = redisService; + _defaultExpiry = TimeSpan.FromMinutes(options.Value.LifetimeMinutes); } public async Task> GetShoppingCartByEmailAsync(string customerEmail) @@ -25,7 +28,7 @@ public async Task> GetShoppingCartByEmailAsync(string custo try { cart = await _redisService.GetObjectAsync(cartKey); - await _redisService.KeyExpireAsync(cartKey, DefaultExpiry); + await _redisService.KeyExpireAsync(cartKey, _defaultExpiry); } catch (Exception e) { @@ -41,7 +44,7 @@ public async Task UpdateShoppingCartAsync(string customerEmail, Shopping try { - var res = await _redisService.SetObjectAsync(cartKey, shoppingCart, DefaultExpiry); + var res = await _redisService.SetObjectAsync(cartKey, shoppingCart, _defaultExpiry); if (!res) { return Result.Failure(StatusCodes.Status500InternalServerError, "the shopping cart could not be updated"); diff --git a/TickAPI/TickAPI/appsettings.example.json b/TickAPI/TickAPI/appsettings.example.json index 420009a..c7e46ab 100644 --- a/TickAPI/TickAPI/appsettings.example.json +++ b/TickAPI/TickAPI/appsettings.example.json @@ -33,5 +33,9 @@ }, "PaymentGateway": { "Url": "http://localhost:7474" + }, + "ShoppingCart": { + "SyncIntervalMinutes": 5, + "LifetimeMinutes": 15 } } \ No newline at end of file From 5eb6440e5f162078afe0682d73dfba591c9bbe6a Mon Sep 17 00:00:00 2001 From: kubapoke Date: Sun, 25 May 2025 01:31:04 +0200 Subject: [PATCH 79/80] updated tests to account for ToObjectResult method change --- TickAPI/TickAPI.Tests/Common/Results/ResultTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TickAPI/TickAPI.Tests/Common/Results/ResultTests.cs b/TickAPI/TickAPI.Tests/Common/Results/ResultTests.cs index 32ed274..828d4b3 100644 --- a/TickAPI/TickAPI.Tests/Common/Results/ResultTests.cs +++ b/TickAPI/TickAPI.Tests/Common/Results/ResultTests.cs @@ -122,7 +122,7 @@ public void ToObjectResult_WhenResultIsSuccess_ShouldReturnObjectResultWithDefau // Assert Assert.IsType(objectResult); Assert.Equal(StatusCodes.Status200OK, objectResult.StatusCode); - Assert.Null(objectResult.Value); + Assert.Equal(string.Empty, objectResult.Value); } [Fact] @@ -138,6 +138,6 @@ public void ToObjectResult_WhenResultIsSuccessWithCustomStatusCode_ShouldReturnO // Assert Assert.IsType(objectResult); Assert.Equal(customSuccessCode, objectResult.StatusCode); - Assert.Null(objectResult.Value); + Assert.Equal(string.Empty, objectResult.Value); } } \ No newline at end of file From 5b2fc9ab9837bc3c70897fe2fdd41a4292afe1cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw?= <62651497+staszkiet@users.noreply.github.com> Date: Sun, 25 May 2025 19:51:05 +0200 Subject: [PATCH 80/80] apply changes --- .../Tickets/Services/TicketServiceTests.cs | 12 ++--- .../Migrations/20250524104655_resellprice.cs | 28 ----------- ...r.cs => 20250525174930_resell.Designer.cs} | 8 ++-- .../Migrations/20250525174930_resell.cs | 46 +++++++++++++++++++ .../TickApiDbContextModelSnapshot.cs | 21 +++++---- .../Tickets/Abstractions/ITicketRepository.cs | 2 +- .../Tickets/Abstractions/ITicketService.cs | 2 +- .../Tickets/Controllers/TicketsController.cs | 6 +-- .../DTOs/Request/SetTicketForResellDataDto.cs | 3 +- TickAPI/TickAPI/Tickets/Models/Ticket.cs | 1 + .../Tickets/Repositories/TicketRepository.cs | 3 +- .../TickAPI/Tickets/Services/TicketService.cs | 4 +- 12 files changed, 81 insertions(+), 55 deletions(-) delete mode 100644 TickAPI/TickAPI/Migrations/20250524104655_resellprice.cs rename TickAPI/TickAPI/Migrations/{20250524104655_resellprice.Designer.cs => 20250525174930_resell.Designer.cs} (98%) create mode 100644 TickAPI/TickAPI/Migrations/20250525174930_resell.cs diff --git a/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs b/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs index 548175e..de24569 100644 --- a/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Tickets/Services/TicketServiceTests.cs @@ -651,7 +651,7 @@ public async Task SetTicketForResellAsync_ReturnsFailure_WhenPriceIsZero() var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, shoppingCartRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); - var result = await sut.SetTicketForResellAsync(Guid.NewGuid(), "test@example.com", 0); + var result = await sut.SetTicketForResellAsync(Guid.NewGuid(), "test@example.com", 0, "zl"); Assert.True(result.IsError); Assert.Equal(500, result.StatusCode); @@ -672,7 +672,7 @@ public async Task SetTicketForResellAsync_ReturnsFailure_WhenPriceTooHigh() var shoppingCartRepositoryMock = new Mock(); var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, shoppingCartRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); - var result = await sut.SetTicketForResellAsync(Guid.NewGuid(), "test@example.com", 160); + var result = await sut.SetTicketForResellAsync(Guid.NewGuid(), "test@example.com", 160, "zl"); Assert.True(result.IsError); Assert.Equal(500, result.StatusCode); @@ -692,7 +692,7 @@ public async Task SetTicketForResellAsync_ReturnsFailure_WhenAlreadyForResell() var shoppingCartRepositoryMock = new Mock(); var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, shoppingCartRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); - var result = await sut.SetTicketForResellAsync(Guid.NewGuid(), "test@example.com", 120); + var result = await sut.SetTicketForResellAsync(Guid.NewGuid(), "test@example.com", 120, "zl"); Assert.True(result.IsError); Assert.Equal(500, result.StatusCode); @@ -714,7 +714,7 @@ public async Task SetTicketForResellAsync_ReturnsFailure_WhenTicketIsUsed() var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, shoppingCartRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); - var result = await sut.SetTicketForResellAsync(Guid.NewGuid(), "test@example.com", 120); + var result = await sut.SetTicketForResellAsync(Guid.NewGuid(), "test@example.com", 120, "zl"); Assert.True(result.IsError); Assert.Equal(500, result.StatusCode); @@ -732,7 +732,7 @@ public async Task SetTicketForResellAsync_ReturnsSuccess_WhenValid() .ReturnsAsync(Result.Success(ticket)); ticketRepositoryMock - .Setup(r => r.SetTicketForResell(It.IsAny(), It.IsAny())) + .Setup(r => r.SetTicketForResell(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(Result.Success()); var ticketTypeRepositoryMock = new Mock(); @@ -740,7 +740,7 @@ public async Task SetTicketForResellAsync_ReturnsSuccess_WhenValid() var sut = new TicketService(ticketRepositoryMock.Object, ticketTypeRepositoryMock.Object, shoppingCartRepositoryMock.Object, paginationServiceMock.Object, qrServiceMock.Object); - var result = await sut.SetTicketForResellAsync(Guid.NewGuid(), "test@example.com", 130); + var result = await sut.SetTicketForResellAsync(Guid.NewGuid(), "test@example.com", 130, "zl"); Assert.False(result.IsError); } diff --git a/TickAPI/TickAPI/Migrations/20250524104655_resellprice.cs b/TickAPI/TickAPI/Migrations/20250524104655_resellprice.cs deleted file mode 100644 index db156f8..0000000 --- a/TickAPI/TickAPI/Migrations/20250524104655_resellprice.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace TickAPI.Migrations -{ - /// - public partial class resellprice : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "ResellPrice", - table: "Tickets", - type: "decimal(18,2)", - nullable: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "ResellPrice", - table: "Tickets"); - } - } -} diff --git a/TickAPI/TickAPI/Migrations/20250524104655_resellprice.Designer.cs b/TickAPI/TickAPI/Migrations/20250525174930_resell.Designer.cs similarity index 98% rename from TickAPI/TickAPI/Migrations/20250524104655_resellprice.Designer.cs rename to TickAPI/TickAPI/Migrations/20250525174930_resell.Designer.cs index ad10aaa..7a09fdb 100644 --- a/TickAPI/TickAPI/Migrations/20250524104655_resellprice.Designer.cs +++ b/TickAPI/TickAPI/Migrations/20250525174930_resell.Designer.cs @@ -12,8 +12,8 @@ namespace TickAPI.Migrations { [DbContext(typeof(TickApiDbContext))] - [Migration("20250524104655_resellprice")] - partial class resellprice + [Migration("20250525174930_resell")] + partial class resell { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -279,12 +279,14 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("bit"); b.Property("NameOnTicket") - .IsRequired() .HasColumnType("nvarchar(max)"); b.Property("OwnerId") .HasColumnType("uniqueidentifier"); + b.Property("ResellCurrency") + .HasColumnType("nvarchar(max)"); + b.Property("ResellPrice") .HasColumnType("decimal(18,2)"); diff --git a/TickAPI/TickAPI/Migrations/20250525174930_resell.cs b/TickAPI/TickAPI/Migrations/20250525174930_resell.cs new file mode 100644 index 0000000..2c65ace --- /dev/null +++ b/TickAPI/TickAPI/Migrations/20250525174930_resell.cs @@ -0,0 +1,46 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TickAPI.Migrations +{ + /// + public partial class resell : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "NameOnTicket", + table: "Tickets", + type: "nvarchar(max)", + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(max)"); + + migrationBuilder.AddColumn( + name: "ResellCurrency", + table: "Tickets", + type: "nvarchar(max)", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ResellCurrency", + table: "Tickets"); + + migrationBuilder.AlterColumn( + name: "NameOnTicket", + table: "Tickets", + type: "nvarchar(max)", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "nvarchar(max)", + oldNullable: true); + } + } +} diff --git a/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs b/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs index 09ceb2d..71d888e 100644 --- a/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs +++ b/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs @@ -34,7 +34,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("EventsId"); - b.ToTable("CategoryEvent", (string)null); + b.ToTable("CategoryEvent"); }); modelBuilder.Entity("TickAPI.Addresses.Models.Address", b => @@ -66,7 +66,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.ToTable("Addresses", (string)null); + b.ToTable("Addresses"); }); modelBuilder.Entity("TickAPI.Admins.Models.Admin", b => @@ -85,7 +85,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.ToTable("Admins", (string)null); + b.ToTable("Admins"); }); modelBuilder.Entity("TickAPI.Categories.Models.Category", b => @@ -100,7 +100,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.ToTable("Categories", (string)null); + b.ToTable("Categories"); b.HasData( new @@ -157,7 +157,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.ToTable("Customers", (string)null); + b.ToTable("Customers"); }); modelBuilder.Entity("TickAPI.Events.Models.Event", b => @@ -198,7 +198,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("OrganizerId"); - b.ToTable("Events", (string)null); + b.ToTable("Events"); }); modelBuilder.Entity("TickAPI.Organizers.Models.Organizer", b => @@ -230,7 +230,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.ToTable("Organizers", (string)null); + b.ToTable("Organizers"); }); modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => @@ -263,7 +263,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("EventId"); - b.ToTable("TicketTypes", (string)null); + b.ToTable("TicketTypes"); }); modelBuilder.Entity("TickAPI.Tickets.Models.Ticket", b => @@ -281,6 +281,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("OwnerId") .HasColumnType("uniqueidentifier"); + b.Property("ResellCurrency") + .HasColumnType("nvarchar(max)"); + b.Property("ResellPrice") .HasColumnType("decimal(18,2)"); @@ -299,7 +302,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("TypeId"); - b.ToTable("Tickets", (string)null); + b.ToTable("Tickets"); }); modelBuilder.Entity("CategoryEvent", b => diff --git a/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs b/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs index 11be49e..4add328 100644 --- a/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs +++ b/TickAPI/TickAPI/Tickets/Abstractions/ITicketRepository.cs @@ -12,6 +12,6 @@ public interface ITicketRepository public IQueryable GetTicketsByEventId(Guid eventId); public IQueryable GetTicketsByCustomerEmail(string email); public Task MarkTicketAsUsed(Guid id); - public Task SetTicketForResell(Guid ticketId, decimal newPrice); + public Task SetTicketForResell(Guid ticketId, decimal newPrice, string currency); public Task AddTicketAsync(Ticket ticket); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs b/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs index 4ee5ef4..1fce588 100644 --- a/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs +++ b/TickAPI/TickAPI/Tickets/Abstractions/ITicketService.cs @@ -21,7 +21,7 @@ public Task>> GetTicketsForCustome public Task> GetTicketDetailsAsync(Guid ticketGuid, string email, string scanUrl); - public Task SetTicketForResellAsync(Guid ticketId, string email, decimal resellPrice); + public Task SetTicketForResellAsync(Guid ticketId, string email, decimal resellPrice, string resellCurrency); public Task> GetTicketTypeByIdAsync(Guid ticketTypeId); public Task CreateTicketAsync(TicketType type, Customer owner, string? nameOnTicket = null, string? seats = null); diff --git a/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs b/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs index a39dc32..54209b7 100644 --- a/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs +++ b/TickAPI/TickAPI/Tickets/Controllers/TicketsController.cs @@ -64,16 +64,16 @@ public async Task> ScanTicket(Guid id) return res.ToObjectResult(); } + [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] [HttpPost("resell/{id:guid}")] - - public async Task> SetTicketForResell(Guid id, [FromBody] SetTicketForResellDataDto data) + public async Task> SetTicketForResell([FromRoute] Guid id, [FromBody] SetTicketForResellDataDto data) { var emailResult = _claimsService.GetEmailFromClaims(User.Claims); if (emailResult.IsError) { return emailResult.ToObjectResult(); } - var res = await _ticketService.SetTicketForResellAsync(id, emailResult.Value!, data.ResellPrice); + var res = await _ticketService.SetTicketForResellAsync(id, emailResult.Value!, data.ResellPrice, data.ResellCurrency); return res.ToObjectResult(); } diff --git a/TickAPI/TickAPI/Tickets/DTOs/Request/SetTicketForResellDataDto.cs b/TickAPI/TickAPI/Tickets/DTOs/Request/SetTicketForResellDataDto.cs index 0058278..7ee6d82 100644 --- a/TickAPI/TickAPI/Tickets/DTOs/Request/SetTicketForResellDataDto.cs +++ b/TickAPI/TickAPI/Tickets/DTOs/Request/SetTicketForResellDataDto.cs @@ -2,5 +2,6 @@ public record SetTicketForResellDataDto ( - decimal ResellPrice + decimal ResellPrice, + string ResellCurrency ); \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Models/Ticket.cs b/TickAPI/TickAPI/Tickets/Models/Ticket.cs index 31ffd27..3622cc6 100644 --- a/TickAPI/TickAPI/Tickets/Models/Ticket.cs +++ b/TickAPI/TickAPI/Tickets/Models/Ticket.cs @@ -12,5 +12,6 @@ public class Ticket public string? Seats { get; set; } public bool ForResell { get; set; } public decimal? ResellPrice { get; set; } + public string? ResellCurrency { get; set; } public bool Used { get; set; } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs b/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs index a365bfc..c3d3911 100644 --- a/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs +++ b/TickAPI/TickAPI/Tickets/Repositories/TicketRepository.cs @@ -88,7 +88,7 @@ public async Task AddTicketAsync(Ticket ticket) return Result.Success(); } - public async Task SetTicketForResell(Guid ticketId, decimal newPrice) + public async Task SetTicketForResell(Guid ticketId, decimal newPrice, string currency) { var ticket = await _tickApiDbContext.Tickets.FirstOrDefaultAsync(t => t.Id == ticketId); if (ticket == null) @@ -96,6 +96,7 @@ public async Task SetTicketForResell(Guid ticketId, decimal newPrice) return Result.Failure(StatusCodes.Status404NotFound, "Ticket with this id doesn't exist"); } ticket.ForResell = true; + ticket.ResellCurrency = currency; ticket.ResellPrice = newPrice; await _tickApiDbContext.SaveChangesAsync(); return Result.Success(); diff --git a/TickAPI/TickAPI/Tickets/Services/TicketService.cs b/TickAPI/TickAPI/Tickets/Services/TicketService.cs index f6d82ee..c643424 100644 --- a/TickAPI/TickAPI/Tickets/Services/TicketService.cs +++ b/TickAPI/TickAPI/Tickets/Services/TicketService.cs @@ -185,7 +185,7 @@ public async Task ScanTicket(Guid ticketGuid) return res; } - public async Task SetTicketForResellAsync(Guid ticketId, string email, decimal resellPrice) + public async Task SetTicketForResellAsync(Guid ticketId, string email, decimal resellPrice, string resellCurrency) { if (resellPrice <= 0) { @@ -213,7 +213,7 @@ public async Task SetTicketForResellAsync(Guid ticketId, string email, d return Result.Failure(StatusCodes.Status500InternalServerError, "Ticket is already used"); } - var res = await _ticketRepository.SetTicketForResell(ticketId, resellPrice); + var res = await _ticketRepository.SetTicketForResell(ticketId, resellPrice, resellCurrency); return res; } } \ No newline at end of file