diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/CustomerGetDto.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/CustomerGetDto.cs new file mode 100644 index 00000000..cf79e21f --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/CustomerGetDto.cs @@ -0,0 +1,11 @@ +namespace api_cinema_challenge.DTOs; + +public class CustomerGetDto +{ + public int Id { get; set; } + public string Name { get; set; } = null!; + public string Email { get; set; } = null!; + public string Phone { get; set; } = null!; + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } +} \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/CustomerPostDto.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/CustomerPostDto.cs new file mode 100644 index 00000000..d2ad5305 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/CustomerPostDto.cs @@ -0,0 +1,8 @@ +namespace api_cinema_challenge.DTOs; + +public class CustomerPostDto +{ + public required string Name { get; set; } + public required string Email { get; set; } + public required string Phone { get; set; } +} \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/CustomerUpdateDto.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/CustomerUpdateDto.cs new file mode 100644 index 00000000..624ef69e --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/CustomerUpdateDto.cs @@ -0,0 +1,8 @@ +namespace api_cinema_challenge.DTOs; + +public class CustomerUpdateDto +{ + public string? Name { get; set; } + public string? Email { get; set; } + public string? Phone { get; set; } +} \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/MovieGetDto.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/MovieGetDto.cs new file mode 100644 index 00000000..9320f33b --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/MovieGetDto.cs @@ -0,0 +1,12 @@ +namespace api_cinema_challenge.DTOs; + +public class MovieGetDto +{ + public int Id { get; set; } + public string Title { get; set; } + public double Rating { get; set; } + public string Description { get; set; } + public int RuntimeMins { get; set; } + public DateTime UpdatedAt { get; set; } + public DateTime CreatedAt { get; set; } +} \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/MoviePostDto.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/MoviePostDto.cs new file mode 100644 index 00000000..2ab430f8 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/MoviePostDto.cs @@ -0,0 +1,12 @@ +using api_cinema_challenge.Models; + +namespace api_cinema_challenge.DTOs; + +public class MoviePostDto +{ + public required string Title { get; set; } + public required double Rating { get; set; } + public required string Description { get; set; } + public required int RuntimeMins { get; set; } + public required ICollection Screenings { get; set; } +} \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/MoviePostScreeningDto.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/MoviePostScreeningDto.cs new file mode 100644 index 00000000..934d60ae --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/MoviePostScreeningDto.cs @@ -0,0 +1,10 @@ +namespace api_cinema_challenge.DTOs; + +public class MoviePostScreeningDto +{ + public int ScreenNumber { get; set; } + + public int Capacity { get; set; } + + public DateTime StartsAt { get; set; } +} \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/MovieUpdateDto.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/MovieUpdateDto.cs new file mode 100644 index 00000000..65e5b2ed --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/MovieUpdateDto.cs @@ -0,0 +1,9 @@ +namespace api_cinema_challenge.DTOs; + +public class MovieUpdateDto +{ + public string? Title { get; set; } + public double? Rating { get; set; } + public string? Description { get; set; } + public int? RuntimeMins { get; set; } +} \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/NumSeatsDto.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/NumSeatsDto.cs new file mode 100644 index 00000000..91b7e32c --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/NumSeatsDto.cs @@ -0,0 +1,6 @@ +namespace api_cinema_challenge.DTOs; + +public class NumSeatsDto +{ + public required int NumSeats { get; set; } +} \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningGetDto.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningGetDto.cs new file mode 100644 index 00000000..620e3c73 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningGetDto.cs @@ -0,0 +1,11 @@ +namespace api_cinema_challenge.DTOs; + +public class ScreeningGetDto +{ + public int Id { get; set; } + public int ScreenNumber { get; set; } + public int Capacity { get; set; } + public DateTime StartsAt { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } +} \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningPostDto.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningPostDto.cs new file mode 100644 index 00000000..65c92773 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/ScreeningPostDto.cs @@ -0,0 +1,8 @@ +namespace api_cinema_challenge.DTOs; + +public class ScreeningPostDto +{ + public required int ScreenNumber { get; set; } + public required int Capacity { get; set; } + public required DateTime StartsAt { get; set; } +} \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/DTOs/TicketGetDto.cs b/api-cinema-challenge/api-cinema-challenge/DTOs/TicketGetDto.cs new file mode 100644 index 00000000..af52edaf --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/DTOs/TicketGetDto.cs @@ -0,0 +1,9 @@ +namespace api_cinema_challenge.DTOs; + +public class TicketGetDto +{ + public int Id { get; set; } + public int NumSeats { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } +} \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs b/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs deleted file mode 100644 index ad4fe854..00000000 --- a/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Newtonsoft.Json.Linq; - -namespace api_cinema_challenge.Data -{ - public class CinemaContext : DbContext - { - private string _connectionString; - public CinemaContext(DbContextOptions options) : base(options) - { - var configuration = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build(); - _connectionString = configuration.GetValue("ConnectionStrings:DefaultConnectionString")!; - this.Database.EnsureCreated(); - } - - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - optionsBuilder.UseNpgsql(_connectionString); - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - - } - } -} diff --git a/api-cinema-challenge/api-cinema-challenge/Data/CinemaDbContext.cs b/api-cinema-challenge/api-cinema-challenge/Data/CinemaDbContext.cs new file mode 100644 index 00000000..055ed61e --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Data/CinemaDbContext.cs @@ -0,0 +1,61 @@ +using System.Diagnostics; +using api_cinema_challenge.Models; +using Microsoft.EntityFrameworkCore; + +namespace api_cinema_challenge.Data +{ + public sealed class CinemaDbContext : DbContext + { + private readonly string _connectionString; + + public CinemaDbContext(DbContextOptions options) : base(options) + { + var configuration = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build(); + _connectionString = configuration.GetValue("ConnectionStrings:DefaultConnection")!; + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + Seeder seeds = new Seeder(); + + // One-to-many relationships + // One customer to many tickets + modelBuilder.Entity() + .HasMany(c => c.Tickets) + .WithOne(t => t.Customer) + .HasForeignKey(t => t.CustomerId) + .OnDelete(DeleteBehavior.Cascade); + + //Many tickets to one screening + modelBuilder.Entity() + .HasMany(c => c.Tickets) + .WithOne(t => t.Screening) + .HasForeignKey(t => t.ScreeningId) + .OnDelete(DeleteBehavior.Cascade); + + // Many screenings to one movie + modelBuilder.Entity() + .HasMany(m => m.Screenings) + .WithOne(s => s.Movie) + .HasForeignKey(s => s.MovieId) + .OnDelete(DeleteBehavior.Cascade); + + // Seed data + modelBuilder.Entity().HasData(seeds.Movies); + modelBuilder.Entity().HasData(seeds.Customers); + modelBuilder.Entity().HasData(seeds.Screenings); + modelBuilder.Entity().HasData(seeds.Tickets); + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseNpgsql(_connectionString); + optionsBuilder.LogTo(message => Debug.WriteLine(message)); + } + + public DbSet Movies { get; set; } + public DbSet Customers { get; set; } + public DbSet Screenings { get; set; } + public DbSet Tickets { get; set; } + } +} \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/Data/CustomerSeed.cs b/api-cinema-challenge/api-cinema-challenge/Data/CustomerSeed.cs new file mode 100644 index 00000000..75561341 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Data/CustomerSeed.cs @@ -0,0 +1,112 @@ +// Data/CustomerSeeder.cs + +using System.Linq; +using api_cinema_challenge.Models; + +namespace api_cinema_challenge.Data; +public class CustomerSeed +{ + private List _firstNames = + [ + "John", "Jane", "Alice", "Bob", "Charlie", "Diana", "Ethan", "Fiona", + "George", "Hannah", "Ian", "Julia", "Kevin", "Laura", "Mike", "Nina" + ]; + + private List _lastNames = + [ + "Smith", "Doe", "Johnson", "Brown", "Williams", "Miller", "Davis", "Wilson", + "Taylor", "Anderson", "Thomas", "Jackson", "White", "Harris", "Martin", "Thompson" + ]; + + private List _domain = new List() + { + "bbc.co.uk", + "google.com", + "theworld.ca", + "something.com", + "tesla.com", + "nasa.org.us", + "gov.us", + "gov.gr", + "gov.nl", + "gov.ru" + }; + + // Builds a deterministic list of customers with unique emails and phone numbers. + public List Get(int take = 10) + { + var customers = new List(take); + int id = 1; + + foreach (var first in _firstNames) + { + foreach (var last in _lastNames) + { + if (customers.Count >= take) return customers; + + var domain = _domain[(id - 1) % _domain.Count]; + + customers.Add(new Customer + { + Id = id, + Name = first + " " + last, + Email = BuildEmail(first, last, id, domain), + Phone = BuildPhone(id, domain) + }); + + id++; + } + } + + // If 'take' exceeds the name combinations, keep cycling + while (customers.Count < take) + { + var first = _firstNames[(id - 1) % _firstNames.Count]; + var last = _lastNames[(id - 1) % _lastNames.Count]; + var domain = _domain[(id - 1) % _domain.Count]; + + customers.Add(new Customer + { + Id = id, + Name = first + " " + last, + Email = BuildEmail(first, last, id, domain), + Phone = BuildPhone(id, domain) + }); + + id++; + } + + return customers; + } + + private static string BuildEmail(string first, string last, int id, string domain) + { + string Slug(string s) => new string(s.ToLowerInvariant().Where(char.IsLetterOrDigit).ToArray()); + return $"{Slug(first)}.{Slug(last)}{id}@{domain}"; + } + + private static string BuildPhone(int id, string domain) + { + // Map TLD to a country code; default to +47 (Norway) + var tld = domain.Split('.').Last(); + var cc = tld switch + { + "us" => "1", + "uk" => "44", + "ca" => "1", + "gr" => "30", + "nl" => "31", + "ru" => "7", + "com" => "1", + "org" => "1", + _ => "47" + }; + + // Deterministic 3-3-4 style digits to ensure uniqueness and validity-like length + int a = (id * 7919) % 900 + 100; // 3 digits + int b = (id * 104729) % 900 + 100; // 3 digits + int c = (id * 13007) % 9000 + 1000; // 4 digits + + return $"+{cc}{a}{b}{c}"; + } +} \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/Data/MovieSeed.cs b/api-cinema-challenge/api-cinema-challenge/Data/MovieSeed.cs new file mode 100644 index 00000000..76ad2434 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Data/MovieSeed.cs @@ -0,0 +1,31 @@ +//Brought to you by ChatGPT +using api_cinema_challenge.Models; + +namespace api_cinema_challenge.Data; + +public static class MovieSeed +{ + public static List Get() => + [ + new Movie { Id = 1, Title = "The Shawshank Redemption", Description = "A banker forms an unlikely friendship in prison while quietly plotting freedom.", RuntimeMins = 142, Rating = 9.3 }, + new Movie { Id = 2, Title = "The Godfather", Description = "A powerful crime family faces shifting loyalties as a reluctant son takes the reins.", RuntimeMins = 175, Rating = 9.2 }, + new Movie { Id = 3, Title = "The Dark Knight", Description = "Batman confronts a chaotic new foe whose plans push Gotham and its hero to the brink.", RuntimeMins = 152, Rating = 9.0 }, + new Movie { Id = 4, Title = "Pulp Fiction", Description = "Intertwined tales of hitmen, a boxer, and a briefcase collide in offbeat fashion.", RuntimeMins = 154, Rating = 8.9 }, + new Movie { Id = 5, Title = "Inception", Description = "A thief who steals secrets through dreams attempts one last, impossible heist.", RuntimeMins = 148, Rating = 8.8 }, + new Movie { Id = 6, Title = "The Matrix", Description = "A hacker discovers a hidden reality and fights to free humanity from control.", RuntimeMins = 136, Rating = 8.7 }, + new Movie { Id = 7, Title = "Parasite", Description = "Two families from different worlds become entangled in a sharp social thriller.", RuntimeMins = 132, Rating = 8.5 }, + new Movie { Id = 8, Title = "Spirited Away", Description = "A girl navigates a spirit world to rescue her parents and find her courage.", RuntimeMins = 125, Rating = 8.6 }, + new Movie { Id = 9, Title = "Gladiator", Description = "A betrayed general rises as a gladiator seeking justice against a corrupt emperor.", RuntimeMins = 155, Rating = 8.5 }, + new Movie { Id = 10, Title = "Interstellar", Description = "Explorers venture through a wormhole to secure a future for humanity.", RuntimeMins = 169, Rating = 8.6 }, + new Movie { Id = 11, Title = "The Lord of the Rings: The Fellowship of the Ring", Description = "A humble hero leads allies on a perilous quest to destroy a corrupting ring.", RuntimeMins = 178, Rating = 8.8 }, + new Movie { Id = 12, Title = "The Silence of the Lambs", Description = "An FBI trainee seeks a killer’s profile with help from an imprisoned genius.", RuntimeMins = 118, Rating = 8.6 }, + new Movie { Id = 13, Title = "Forrest Gump", Description = "A kind-hearted man unwittingly drifts through historic moments while chasing love.", RuntimeMins = 142, Rating = 8.8 }, + new Movie { Id = 14, Title = "Fight Club", Description = "An insomniac’s underground club spirals into a manifesto against modern life.", RuntimeMins = 139, Rating = 8.8 }, + new Movie { Id = 15, Title = "The Social Network", Description = "Friendships fracture as the creation of a global platform triggers legal battles.", RuntimeMins = 120, Rating = 7.8 }, + new Movie { Id = 16, Title = "Whiplash", Description = "An ambitious drummer faces a ruthless mentor in a battle of will and rhythm.", RuntimeMins = 107, Rating = 8.5 }, + new Movie { Id = 17, Title = "Mad Max: Fury Road", Description = "A high-octane desert chase pits rebels against a tyrant’s armored war party.", RuntimeMins = 120, Rating = 8.1 }, + new Movie { Id = 18, Title = "La La Land", Description = "An actress and a jazz pianist chase dreams and reckon with the cost of ambition.", RuntimeMins = 128, Rating = 8.0 }, + new Movie { Id = 19, Title = "The Grand Budapest Hotel", Description = "A fastidious concierge and his protégé dash through a caper over a priceless painting.", RuntimeMins = 100, Rating = 8.1 }, + new Movie { Id = 20, Title = "Get Out", Description = "A weekend visit unravels into a sharp, unsettling examination of control and identity.", RuntimeMins = 104, Rating = 7.8 } + ]; +} diff --git a/api-cinema-challenge/api-cinema-challenge/Data/ScreeningSeed.cs b/api-cinema-challenge/api-cinema-challenge/Data/ScreeningSeed.cs new file mode 100644 index 00000000..697a1fba --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Data/ScreeningSeed.cs @@ -0,0 +1,56 @@ +//Brought to you by ChatGPT +using api_cinema_challenge.Models; + +namespace api_cinema_challenge.Data; + +public static class ScreeningSeed +{ + public static List Get() + { + // Source movies + var movies = MovieSeed.Get(); + + // Config (tweak as needed) + var baseStartUtc = DateTime.UtcNow; + const int days = 2; // how many days to seed + const int showsPerDayPerScreen = 4; // shows on each screen per day + const int cleaningBufferMins = 20; // gap between shows, per screen + int[] capacities = { 80, 120, 180, 120, 60 }; // screen 1..5 capacities + + var screenings = new List(days * capacities.Length * showsPerDayPerScreen); + + var id = 1; + var movieIndex = 0; + + for (var d = 0; d < days; d++) + { + var dayStart = baseStartUtc.AddDays(d); + + for (var s = 0; s < capacities.Length; s++) + { + var screenNumber = s + 1; + var slotStart = dayStart; + + for (var show = 0; show < showsPerDayPerScreen; show++) + { + var movie = movies[movieIndex % movies.Count]; + + screenings.Add(new Screening + { + Id = id++, + MovieId = movie.Id, + ScreenNumber = screenNumber, + Capacity = capacities[s], + StartsAt = slotStart + }); + + // Advance start time for this screen by the movie length + cleaning buffer + slotStart = slotStart.AddMinutes(movie.RuntimeMins + cleaningBufferMins); + movieIndex++; + } + } + } + + return screenings; + } +} \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/Data/Seeder.cs b/api-cinema-challenge/api-cinema-challenge/Data/Seeder.cs new file mode 100644 index 00000000..66d25cdb --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Data/Seeder.cs @@ -0,0 +1,43 @@ +using api_cinema_challenge.Models; + +namespace api_cinema_challenge.Data; + +public class Seeder +{ + public Seeder() + { + Movies = MovieSeed.Get(); + Customers = new CustomerSeed().Get(10); + Screenings = ScreeningSeed.Get(); + Tickets = CreateTickets(); + } + + private List CreateTickets() + { + var random = new Random(123); + const int numTickets = 12; + + var results = new List(numTickets); + for (var i = 0; i < numTickets; i++) + { + var screening = Screenings[random.Next(Screenings.Count)]; + var customer = Customers[random.Next(Customers.Count)]; + + results.Add(new Ticket + { + Id = i + 1, + NumSeats = random.Next(1, 5), + ScreeningId = screening.Id, + CustomerId = customer.Id, + }); + } + + return results; + } + + public List Movies { get; } + public List Screenings { get; } + public List Customers { get; } + public List Tickets { get; } + +} \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoint.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoint.cs new file mode 100644 index 00000000..d12bca09 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoint.cs @@ -0,0 +1,127 @@ +using api_cinema_challenge.DTOs; +using api_cinema_challenge.Repository; + +namespace api_cinema_challenge.Endpoints; + +public static class CustomerEndpoint +{ + public static void ConfigureCustomerEndpoint(this IEndpointRouteBuilder routes) + { + var group = routes + .MapGroup("/customers") + .WithTags("Customers") + .WithSummary("Customers API") + .WithDescription("This API allows you to manage customers at the cinema.") + .WithOpenApi(); + + group.MapGet("/", GetCustomers) + .WithName("GetCustomers") + .WithSummary("Get all customers.") + .WithDescription("Retrieves all customers from the cinema database.") + .Produces>(StatusCodes.Status200OK); + + group.MapPut("/", PostCustomer) + .WithName("PostCustomer") + .WithSummary("Create a new customer.") + .WithDescription("Creates a new customer in the cinema database.") + .Accepts("application/json") + .Produces(StatusCodes.Status201Created) + .Produces(StatusCodes.Status400BadRequest); + + group.MapPut("/{id:int}", UpdateCustomer) + .WithName("UpdateCustomer") + .WithSummary("Update a customer.") + .WithDescription("Updates a customer in the cinema database.") + .Accepts("application/json") + .Produces(StatusCodes.Status201Created) + .Produces(StatusCodes.Status400BadRequest); + + group.MapDelete("/{id:int}", DeleteCustomer) + .WithName("DeleteCustomer") + .WithSummary("Delete a customer.") + .WithDescription("Deletes a customer from the cinema database.") + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status400BadRequest); + + group.MapPost("/{customerId:int}/screenings/{screeningId:int}", BookTicket) + .WithName("BookTicket") + .WithSummary("Book a ticket for a customer.") + .WithDescription("Books a ticket for a customer for a specific screening.") + .Accepts("application/json") + .WithTags("Ticket") + .Produces(StatusCodes.Status201Created) + .ProducesProblem(StatusCodes.Status400BadRequest); + + group.MapGet("/{customerId:int}/screenings/{screeningId:int}", GetTickets) + .WithName("GetTickets") + .WithSummary("Get all tickets for a customer and screening.") + .WithTags("Ticket") + .Produces>(StatusCodes.Status201Created); + } + + private static async Task GetTickets(ICustomerRepository repository, int customerId, int screeningId) + { + return TypedResults.Ok(await repository.GetTickets(customerId, screeningId)); + } + + private static async Task BookTicket(ICustomerRepository repository, NumSeatsDto numSeats, int customerId, int screeningId) + { + return TypedResults.Created($"/customers/{customerId}/screenings/{screeningId}", await repository.BookTicket(customerId, screeningId, numSeats)); + } + + private static async Task GetCustomers(ICustomerRepository repository) + { + return TypedResults.Ok(await repository.GetCustomers()); + } + + private static async Task PostCustomer(ICustomerRepository repository, CustomerPostDto cpd) + { + try + { + var c = await repository.PostCustomer(cpd); + return TypedResults.Created($"/customers/", c); + } + catch (ArgumentException e) + { + return TypedResults.BadRequest(e.Message); + } + catch (Exception e) + { + return TypedResults.BadRequest(e.Message); + } + } + + private static async Task UpdateCustomer(ICustomerRepository repository, int id, CustomerUpdateDto cud) + { + try + { + var c = await repository.UpdateCustomer(id, cud); + return TypedResults.Ok(c); + } + catch (ArgumentException e) + { + return TypedResults.BadRequest(e.Message); + } + catch (Exception e) + { + return TypedResults.BadRequest(e.Message); + } + } + + private static async Task DeleteCustomer(ICustomerRepository repository, int id) + { + try + { + var c = await repository.DeleteCustomer(id); + return TypedResults.Ok(c); + } + catch (ArgumentException e) + { + return TypedResults.BadRequest(e.Message); + } + catch (Exception e) + { + return TypedResults.BadRequest(e.Message); + } + } +} \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoint.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoint.cs new file mode 100644 index 00000000..2c077354 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoint.cs @@ -0,0 +1,140 @@ +using api_cinema_challenge.DTOs; +using api_cinema_challenge.Models; +using api_cinema_challenge.Repository; +using Microsoft.AspNetCore.Mvc; + +namespace api_cinema_challenge.Endpoints; + +public static class MovieEndpoint +{ + public static void ConfigureMovieEndpoint(this IEndpointRouteBuilder routes) + { + var group = routes + .MapGroup("/movies") + .WithTags("Movie") + .WithSummary("Movies API") + .WithDescription("This API allows you to manage Movies at the cinema.") + .WithOpenApi(); + + group.MapGet("/", GetMovies) + .WithName("GetMovies") + .WithSummary("Get all movies.") + .WithDescription("Retrieves all movies from cinema data base.") + .Produces>(StatusCodes.Status200OK); + + group.MapPost("/", PostMovie) + .WithName("PostMovie") + .WithSummary("Create a new movie.") + .WithDescription("Creates a new movie in the cinema data base.") + .Accepts("application/json") + .Produces(StatusCodes.Status201Created) + .ProducesProblem(StatusCodes.Status400BadRequest); + + group.MapPut("/{id:int}", UpdateMovie) + .WithName("UpdateMovie") + .WithSummary("Update an existing movie.") + .WithDescription("Updates an existing movie in the cinema data base.") + .Accepts("application/json") + .Produces(StatusCodes.Status201Created) + .ProducesProblem(StatusCodes.Status400BadRequest); + + group.MapDelete("/{id:int}", DeleteMovie) + .WithName("DeleteMovie") + .WithSummary("Delete an existing movie.") + .WithDescription("Deletes an existing movie from the cinema data base.") + .Produces(StatusCodes.Status200OK) + .ProducesProblem(StatusCodes.Status400BadRequest); + + group.MapPost("/{id:int}/screenings/", CreateScreening) + .WithName("CreateScreening") + .WithTags("Screening") + .WithSummary("Create a new screening for an existing movie.") + .Produces(StatusCodes.Status200OK) + .ProducesProblem(StatusCodes.Status400BadRequest); + + group.MapGet("/{id:int}/screenings/", GetScreenings) + .WithName("GetScreenings") + .WithTags("Screening") + .WithSummary("Get all screenings for an existing movie.") + .Produces>(StatusCodes.Status200OK); + + } + + private static async Task GetScreenings(IMovieRepository repository, int id) + { + return TypedResults.Ok(await repository.GetScreenings(id)); + } + + private static async Task CreateScreening(IMovieRepository repository, int id, ScreeningPostDto screening) + { + try + { + var createdScreening = await repository.CreateScreening(id, screening); + return TypedResults.Created($"/movies/{id}/screenings/", createdScreening); + } + catch (ArgumentException e) + { + return TypedResults.Problem(e.Message); + } + catch (Exception e) + { + return TypedResults.Problem("An unknown error occurred while creating the screening."); + } + } + + private static async Task GetMovies(IMovieRepository repository) + { + return TypedResults.Ok(await repository.GetMovies()); + } + + private static async Task PostMovie(IMovieRepository repository, MoviePostDto movie) + { + try + { + var createdMovie = await repository.PostMovie(movie); + return TypedResults.Created("/movies/", createdMovie); + } + catch (ArgumentException e) + { + return TypedResults.Problem(e.Message); + } + catch (Exception e) + { + return TypedResults.Problem("An unknown error occurred while creating the movie."); + } + } + + private static async Task UpdateMovie(IMovieRepository repository, MovieUpdateDto movie, int id) + { + try + { + var createdMovie = await repository.UpdateMovie(id, movie); + return TypedResults.Created($"/movies/", createdMovie); + } + catch (ArgumentException e) + { + return TypedResults.Problem(e.Message); + } + catch (Exception e) + { + return TypedResults.Problem("An unknown error occurred while creating the movie."); + } + } + + private static async Task DeleteMovie(IMovieRepository repository, int id) + { + try + { + var deletedMovie = await repository.DeleteMovie(id); + return TypedResults.Ok(deletedMovie); + } + catch (ArgumentException e) + { + return TypedResults.Problem(e.Message); + } + catch (Exception e) + { + return TypedResults.Problem("An unknown error occurred while deleting the movie."); + } + } +} \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Customer.cs b/api-cinema-challenge/api-cinema-challenge/Models/Customer.cs new file mode 100644 index 00000000..09f6a9c5 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Customer.cs @@ -0,0 +1,30 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Newtonsoft.Json; + +namespace api_cinema_challenge.Models; + +[Table("customer")] +public class Customer +{ + [Column("id"), Required, Key] + public int Id { get; set; } + + [Column("name"), MaxLength(128), Required] + public string Name { get; set; } + + [Column("email"), MaxLength(256), Required] + public string Email { get; set; } + + [Column("phone"), MaxLength(32), Required] + public string Phone { get; set; } + + [Column("created_at"), Required] + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + [Column("updated_at"), Required] + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + + [Column("tickets"), JsonIgnore] + public virtual ICollection Tickets { get; set; } = new List(); +} \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs b/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs new file mode 100644 index 00000000..5c2289fc --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs @@ -0,0 +1,33 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Newtonsoft.Json; + +namespace api_cinema_challenge.Models; + +[Table("movie")] +public class Movie +{ + [Column("id"), Required, Key] + public int Id { get; set; } + + [Column("title"), MaxLength(128), Required] + public string Title { get; set; } + + [Column("rating"), Required] + public double Rating { get; set; } + + [Column("description"), MaxLength(1024), Required] + public string Description { get; set; } + + [Column("run_time_minutes"), Required] + public int RuntimeMins{ get; set; } + + [Column("created_at"), Required] + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + [Column("updated_at"), Required] + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; //Assumes creation counts as an update + + [Column("screenings"), JsonIgnore, Required] + public virtual ICollection Screenings { get; set; } = new List(); +} \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Screening.cs b/api-cinema-challenge/api-cinema-challenge/Models/Screening.cs new file mode 100644 index 00000000..7d5f9d3e --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Screening.cs @@ -0,0 +1,35 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Newtonsoft.Json; + +namespace api_cinema_challenge.Models; + +[Table("screening")] +public class Screening +{ + [Column("id"), Key, Required] + public int Id { get; set; } + + [Column("movie_fk"), ForeignKey("movie"), Required] + public int MovieId { get; set; } + + [Column("movie"), Required] + public Movie Movie { get; set; } + + [Column("screen_number"), Required] + public int ScreenNumber { get; set; } + + [Column("capacity"), Required] + public int Capacity { get; set; } + + [Column("starts_at"), Required] + public DateTime StartsAt { get; set; } + + [Column("created_at"), Required] + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + [Column("Updated_at"), Required] + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + + [Column("tickets"), JsonIgnore] + public virtual ICollection? Tickets { get; set; } = new List(); +} \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs b/api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs new file mode 100644 index 00000000..1ad120ea --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs @@ -0,0 +1,34 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; + +namespace api_cinema_challenge.Models; + +[Table("ticket")] +public class Ticket +{ + [Column("id"), Required, Key] + public int Id { get; set; } + + [Column("num_seats"), Required] + public int NumSeats { get; set; } + + [Column("screening_fk"), ForeignKey("screening"), Required] + public int ScreeningId { get; set; } + + [Column("customer_fk"), ForeignKey("customer"),Required] + public int CustomerId { get; set; } + + [Column("screening"),Required] + [JsonIgnore] + public Screening Screening { get; set; } + + [Column("customer"),Required] + [JsonIgnore] + public Customer Customer { get; set; } + + [Column("created_at"), Required] + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + [Column("Updated_at"), Required] + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; +} \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/Program.cs b/api-cinema-challenge/api-cinema-challenge/Program.cs index e55d9d54..24fda0fb 100644 --- a/api-cinema-challenge/api-cinema-challenge/Program.cs +++ b/api-cinema-challenge/api-cinema-challenge/Program.cs @@ -1,11 +1,15 @@ using api_cinema_challenge.Data; +using api_cinema_challenge.Endpoints; +using api_cinema_challenge.Repository; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); -builder.Services.AddDbContext(); +builder.Services.AddDbContext(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); var app = builder.Build(); @@ -17,4 +21,8 @@ } app.UseHttpsRedirection(); +app.ConfigureMovieEndpoint(); +app.ConfigureCustomerEndpoint(); app.Run(); + +public partial class Program { } \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/Properties/launchSettings.json b/api-cinema-challenge/api-cinema-challenge/Properties/launchSettings.json index 88dd35e0..04b7259d 100644 --- a/api-cinema-challenge/api-cinema-challenge/Properties/launchSettings.json +++ b/api-cinema-challenge/api-cinema-challenge/Properties/launchSettings.json @@ -14,7 +14,7 @@ "dotnetRunMessages": true, "launchBrowser": true, "launchUrl": "swagger", - "applicationUrl": "http://localhost:5059", + "applicationUrl": "http://localhost:4000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } @@ -24,7 +24,7 @@ "dotnetRunMessages": true, "launchBrowser": true, "launchUrl": "swagger", - "applicationUrl": "https://localhost:7195;http://localhost:5059", + "applicationUrl": "https://localhost:8000;http://localhost:4000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/api-cinema-challenge/api-cinema-challenge/Repository/CustomerRepository.cs b/api-cinema-challenge/api-cinema-challenge/Repository/CustomerRepository.cs new file mode 100644 index 00000000..e8e9e9d5 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repository/CustomerRepository.cs @@ -0,0 +1,200 @@ +using api_cinema_challenge.Data; +using api_cinema_challenge.DTOs; +using api_cinema_challenge.Models; +using Microsoft.EntityFrameworkCore; + +namespace api_cinema_challenge.Repository; + +public class CustomerRepository(CinemaDbContext cinemaDb) : ICustomerRepository +{ + public async Task> GetCustomers() + { + return await cinemaDb.Customers + .Select(c => new CustomerGetDto() + { + Id = c.Id, + Name = c.Name, + Email = c.Email, + Phone = c.Phone, + CreatedAt = c.CreatedAt, + UpdatedAt = c.UpdatedAt + }).ToListAsync(); + } + + public async Task PostCustomer(CustomerPostDto cpd) + { + // Check validate post dto + if (string.IsNullOrWhiteSpace(cpd.Name) || + string.IsNullOrWhiteSpace(cpd.Email) || + string.IsNullOrWhiteSpace(cpd.Phone)) + { + throw new ArgumentException("Invalid customer data."); + } + + // Check email not already exists + var existingCustomer = await cinemaDb.Customers + .FirstOrDefaultAsync(c => c.Email == cpd.Email); + + if (existingCustomer != null) + { + throw new ArgumentException("Customer with this email already exists."); + } + + // TODO: check phone + + // Create new customer + var c = cinemaDb.Customers.Add(new Customer + { + Name = cpd.Name, + Email = cpd.Email, + Phone = cpd.Phone, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow, + Tickets = [] + }); + + await cinemaDb.SaveChangesAsync(); + return new CustomerGetDto + { + Id = c.Entity.Id, + Name = c.Entity.Name, + Email = c.Entity.Email, + Phone = c.Entity.Phone, + CreatedAt = c.Entity.CreatedAt, + UpdatedAt = c.Entity.UpdatedAt + }; + } + + public async Task UpdateCustomer(int id, CustomerUpdateDto cud) + { + var customerToUpdate = await cinemaDb.Customers.FirstOrDefaultAsync(c => c.Id == id); + + if (customerToUpdate == null) + { + throw new ArgumentException("Customer not found."); + } + + // Update fields if provided + if (!string.IsNullOrWhiteSpace(cud.Name)) + { + customerToUpdate.Name = cud.Name; + } + + if (!string.IsNullOrWhiteSpace(cud.Email)) + { + // Check email not already exists + var existingCustomer = await cinemaDb.Customers + .FirstOrDefaultAsync(c => c.Email == cud.Email && c.Id != id); + + if (existingCustomer != null) + { + throw new ArgumentException("Another customer with this email already exists."); + } + + customerToUpdate.Email = cud.Email; + } + + if (!string.IsNullOrWhiteSpace(cud.Phone)) + { + customerToUpdate.Phone = cud.Phone; + } + + customerToUpdate.UpdatedAt = DateTime.UtcNow; + + await cinemaDb.SaveChangesAsync(); + + return new CustomerGetDto + { + Id = customerToUpdate.Id, + Name = customerToUpdate.Name, + Email = customerToUpdate.Email, + Phone = customerToUpdate.Phone, + CreatedAt = customerToUpdate.CreatedAt, + UpdatedAt = customerToUpdate.UpdatedAt + }; + } + + public async Task DeleteCustomer(int id) + { + var customerToDelete = await cinemaDb.Customers.FirstOrDefaultAsync(c => c.Id == id); + + if (customerToDelete == null) + { + throw new ArgumentException("Customer not found."); + } + + cinemaDb.Customers.Remove(customerToDelete); + await cinemaDb.SaveChangesAsync(); + return new CustomerGetDto + { + Id = customerToDelete.Id, + Name = customerToDelete.Name, + Email = customerToDelete.Email, + Phone = customerToDelete.Phone, + CreatedAt = customerToDelete.CreatedAt, + UpdatedAt = customerToDelete.UpdatedAt + }; + } + + public async Task BookTicket(int customerId, int screeningId, NumSeatsDto numSeats) + { + var customer = await cinemaDb.Customers + .FirstOrDefaultAsync(c => c.Id == customerId); + + if (customer == null) + { + throw new ArgumentException("Customer not found."); + } + + var screening = await cinemaDb.Screenings.Include(screening => screening.Tickets).FirstOrDefaultAsync(s => s.Id == screeningId); + + if (screening == null) + { + throw new ArgumentException("Screening not found."); + } + + if (numSeats.NumSeats <= 0) + { + throw new ArgumentException("Number of seats must be greater than zero."); + } + + if (screening.Tickets != null && screening.Capacity < screening.Tickets.Count + numSeats.NumSeats) + { + throw new ArgumentException("Not enough seats available."); + } + + var ticket = cinemaDb.Tickets.Add(new Ticket + { + CustomerId = customerId, + ScreeningId = screeningId, + NumSeats = numSeats.NumSeats, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }); + + + await cinemaDb.SaveChangesAsync(); + return new TicketGetDto + { + Id = ticket.Entity.Id, + NumSeats = ticket.Entity.NumSeats, + CreatedAt = ticket.Entity.CreatedAt, + UpdatedAt = ticket.Entity.UpdatedAt + }; + + } + + public async Task> GetTickets(int customerId, int screeningId) + { + return await cinemaDb.Tickets + .Where(t => t.CustomerId == customerId && t.ScreeningId == screeningId) + .Select(t => new TicketGetDto() + { + Id = t.Id, + NumSeats = t.NumSeats, + CreatedAt = t.CreatedAt, + UpdatedAt = t.UpdatedAt + }) + .ToListAsync(); + } +} \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/Repository/ICustomerRepository.cs b/api-cinema-challenge/api-cinema-challenge/Repository/ICustomerRepository.cs new file mode 100644 index 00000000..1ec1dd5b --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repository/ICustomerRepository.cs @@ -0,0 +1,13 @@ +using api_cinema_challenge.DTOs; + +namespace api_cinema_challenge.Repository; + +public interface ICustomerRepository +{ + public Task> GetCustomers(); + public Task PostCustomer(CustomerPostDto cpd); + public Task UpdateCustomer(int id, CustomerUpdateDto cud); + public Task DeleteCustomer(int id); + public Task BookTicket(int customerId, int screeningId, NumSeatsDto numSeats); + public Task> GetTickets(int customerId, int screeningId); +} \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/Repository/IMovieRepository.cs b/api-cinema-challenge/api-cinema-challenge/Repository/IMovieRepository.cs new file mode 100644 index 00000000..32f28b00 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repository/IMovieRepository.cs @@ -0,0 +1,13 @@ +using api_cinema_challenge.DTOs; + +namespace api_cinema_challenge.Repository; + +public interface IMovieRepository +{ + public Task> GetMovies(); + public Task PostMovie(MoviePostDto mpd); + public Task UpdateMovie(int id, MovieUpdateDto mud); + public Task DeleteMovie(int id); + public Task CreateScreening(int movieId, ScreeningPostDto spd); + public Task> GetScreenings(int movieid); +} \ No newline at end of file diff --git a/api-cinema-challenge/api-cinema-challenge/Repository/MovieRepository.cs b/api-cinema-challenge/api-cinema-challenge/Repository/MovieRepository.cs new file mode 100644 index 00000000..2d5d6537 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repository/MovieRepository.cs @@ -0,0 +1,183 @@ +using api_cinema_challenge.Data; +using api_cinema_challenge.DTOs; +using api_cinema_challenge.Models; +using Microsoft.EntityFrameworkCore; + +namespace api_cinema_challenge.Repository; + +public class MovieRepository(CinemaDbContext cinemaDb) : IMovieRepository +{ + public async Task> GetMovies() + { + return await cinemaDb.Movies.Select(m=> new MovieGetDto + { + Id = m.Id, + Title = m.Title, + Rating = m.Rating, + Description = m.Description, + RuntimeMins = m.RuntimeMins, + UpdatedAt = m.UpdatedAt, + CreatedAt = m.CreatedAt + }).ToListAsync(); + } + + public async Task> GetScreenings(int id) + { + var m = await cinemaDb.Movies.FirstOrDefaultAsync(m => m.Id == id); + + if (m == null) + { + throw new ArgumentException("Movie not found."); + } + + return await cinemaDb.Screenings + .Where(s => s.MovieId == id) + .Select(s => new ScreeningGetDto + { + Id = s.Id, + ScreenNumber = s.ScreenNumber, + Capacity = s.Capacity, + StartsAt = s.StartsAt, + CreatedAt = s.CreatedAt, + UpdatedAt = s.UpdatedAt + }).ToListAsync(); + } + + public async Task UpdateMovie(int id, MovieUpdateDto mud) + { + var movieToUpdate = await cinemaDb.Movies.FirstOrDefaultAsync(m => m.Id == id); + + if (movieToUpdate == null) + { + throw new ArgumentException("Movie not found."); + } + + if(mud.Title != null) movieToUpdate.Title = mud.Title; + if(mud.Rating != null) movieToUpdate.Rating = mud.Rating.Value; + if(mud.Description != null) movieToUpdate.Description = mud.Description; + if(mud.RuntimeMins != null) movieToUpdate.RuntimeMins = mud.RuntimeMins.Value; + movieToUpdate.UpdatedAt = DateTime.UtcNow; + + await cinemaDb.SaveChangesAsync(); + + return mud; // Should not be this object as it doesn't conform to the spec. + } + + public async Task DeleteMovie(int id) + { + var entity = await cinemaDb.Movies + .Include(m => m.Screenings) + .FirstOrDefaultAsync(m => m.Id == id); + + if (entity == null) + { + throw new ArgumentException("Movie not found."); + } + + // Should cascade... hopefully + cinemaDb.Movies.Remove(entity); + await cinemaDb.SaveChangesAsync(); + return new MovieGetDto + { + Id = entity.Id, + Title = entity.Title, + Rating = entity.Rating, + Description = entity.Description, + RuntimeMins = entity.RuntimeMins, + UpdatedAt = entity.UpdatedAt, + CreatedAt = entity.CreatedAt + }; + } + + public async Task CreateScreening(int movieId, ScreeningPostDto spd) + { + var m = await cinemaDb.Movies.FirstOrDefaultAsync(m => m.Id == movieId); + + if (m == null) + { + throw new ArgumentException("Movie not found."); + } + + if (spd.ScreenNumber <= 0 || spd.Capacity <= 0 || spd.StartsAt == default) + { + throw new ArgumentException("Invalid screening data."); + } + + var s = cinemaDb.Screenings.Add(new Screening + { + MovieId = movieId, + ScreenNumber = spd.ScreenNumber, + Capacity = spd.Capacity, + StartsAt = spd.StartsAt, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }); + + await cinemaDb.SaveChangesAsync(); + + return new ScreeningGetDto + { + Id = s.Entity.MovieId, + ScreenNumber = s.Entity.ScreenNumber, + Capacity = s.Entity.Capacity, + StartsAt = s.Entity.StartsAt, + CreatedAt = s.Entity.CreatedAt, + UpdatedAt = s.Entity.UpdatedAt + }; + } + + public async Task PostMovie(MoviePostDto mpd) + { + // Validate input + if (string.IsNullOrWhiteSpace(mpd.Title) || + mpd.Rating < 0 || mpd.Rating > 10 || + string.IsNullOrWhiteSpace(mpd.Description) || + mpd.RuntimeMins <= 0) + { + throw new ArgumentException("Invalid input data for movie."); + } + + // Check if movie already exists + var existingMovie = await cinemaDb.Movies + .FirstOrDefaultAsync(m => m.Title == mpd.Title + && m.RuntimeMins == mpd.RuntimeMins); + + if (existingMovie != null) + { + throw new ArgumentException("Movie already exists with the same title and runtime."); + } + + // Create new movie + var m = cinemaDb.Movies.Add(new Movie + { + Title = mpd.Title, + Rating = mpd.Rating, + Description = mpd.Description, + RuntimeMins = mpd.RuntimeMins, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }); + + foreach (var s in mpd.Screenings) + { + // Validate screening data + if (s.ScreenNumber <= 0 || s.Capacity <= 0 || s.StartsAt == default) + { + throw new ArgumentException("Invalid screening data."); + } + + // Create new screening + cinemaDb.Screenings.Add(new Screening + { + MovieId = m.Entity.Id, + Movie = m.Entity, + ScreenNumber = s.ScreenNumber, + Capacity = s.Capacity, + StartsAt = s.StartsAt + }); + } + + await cinemaDb.SaveChangesAsync(); + return mpd; // Should not be this object as it doesn't conform to the spec. + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj b/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj index 11e5c66b..fa98a649 100644 --- a/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj +++ b/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj @@ -27,8 +27,4 @@ - - - - diff --git a/csharp-api-cinema-challenge.drawio.png b/csharp-api-cinema-challenge.drawio.png new file mode 100644 index 00000000..5147d75b Binary files /dev/null and b/csharp-api-cinema-challenge.drawio.png differ