diff --git a/.gitignore b/.gitignore index dc01f204..b86b5dc2 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,5 @@ out/ ### VS Code ### .vscode/ + +src/main/resources/application.yml \ No newline at end of file diff --git a/build.gradle b/build.gradle index 3d7f7607..66ca4b78 100644 --- a/build.gradle +++ b/build.gradle @@ -24,6 +24,15 @@ dependencies { runtimeOnly 'org.postgresql:postgresql' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + // Lombok + compileOnly 'org.projectlombok:lombok:1.18.34' + annotationProcessor 'org.projectlombok:lombok' + // To use @Valid + implementation 'org.springframework.boot:spring-boot-starter-validation' + // MapStruct for mapping DTOs + implementation("org.mapstruct:mapstruct:1.5.3.Final") + annotationProcessor("org.mapstruct:mapstruct-processor:1.5.3.Final") + } tasks.named('test') { diff --git a/src/main/java/com/booleanuk/api/cinema/.gitkeep b/src/main/java/com/booleanuk/api/cinema/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/com/booleanuk/api/cinema/Main.java b/src/main/java/com/booleanuk/api/cinema/Main.java new file mode 100644 index 00000000..6151ef1f --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/Main.java @@ -0,0 +1,11 @@ +package com.booleanuk.api.cinema; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Main { + public static void main(String[] args){ + SpringApplication.run(Main.class, args); + } +} diff --git a/src/main/java/com/booleanuk/api/cinema/customer/controller/CustomerController.java b/src/main/java/com/booleanuk/api/cinema/customer/controller/CustomerController.java new file mode 100644 index 00000000..26796a05 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/customer/controller/CustomerController.java @@ -0,0 +1,157 @@ +package com.booleanuk.api.cinema.customer.controller; + +import com.booleanuk.api.cinema.customer.model.Customer; +import com.booleanuk.api.cinema.customer.model.CustomerResponseDTO; +import com.booleanuk.api.cinema.customer.repository.CustomerRepository; +import com.booleanuk.api.cinema.response.Response; +import com.booleanuk.api.cinema.response.ResponseFactory; +import com.booleanuk.api.cinema.screening.model.Screening; +import com.booleanuk.api.cinema.screening.repository.ScreeningRepository; +import com.booleanuk.api.cinema.ticket.model.Ticket; +import com.booleanuk.api.cinema.ticket.repository.TicketRepository; +import jakarta.validation.Valid; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.*; + +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static com.booleanuk.api.cinema.response.ResponseFactory.*; + +@RestController +@RequestMapping("/customers") +public class CustomerController { + + @Autowired + CustomerRepository customerRepository; + + @Autowired + TicketRepository ticketRepository; + + @Autowired + ScreeningRepository screeningRepository; + + // Workaround for exception 415 + @PostMapping(consumes = {"application/json", "application/json;charset=UTF-8"}) + public ResponseEntity addCustomer(@Valid @RequestBody Customer customer, BindingResult result) { + + if (result.hasErrors()) { + return badRequestErrorResponse(); + } + + Customer savedCustomer = this.customerRepository.save(customer); + CustomerResponseDTO response = convertToCustomerResponseDTO(savedCustomer); + return createdSuccessResponse(response); + } + + @GetMapping + public ResponseEntity getAllCustomers() { + List response = new ArrayList<>(); + this.customerRepository.findAll().forEach(customer -> + response.add(convertToCustomerResponseDTO(customer)) + ); + return okSuccessResponse(response); + } + + @GetMapping("/{id}") + public ResponseEntity getCustomerById(@PathVariable (name = "id") int id) { + return this.customerRepository.findById(id). + map(customer -> { + CustomerResponseDTO response = convertToCustomerResponseDTO(customer); + return okSuccessResponse(response); + }) + .orElseGet(ResponseFactory::notFoundErrorResponse); + } + + @PutMapping("/{id}") + public ResponseEntity updateCustomer(@PathVariable (name = "id") int id, @Valid @RequestBody Customer updatedCustomer, BindingResult result) { + + if (result.hasErrors()) { + return badRequestErrorResponse(); + } + + return this.customerRepository.findById(id).map(customerToUpdate -> { + updateCustomerDetails(customerToUpdate, updatedCustomer); + Customer savedCustomer = this.customerRepository.save(customerToUpdate); + CustomerResponseDTO response = convertToCustomerResponseDTO(savedCustomer); + return createdSuccessResponse(response); + }).orElseGet(ResponseFactory::notFoundErrorResponse); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteCustomer(@PathVariable (name = "id") int id){ + return this.customerRepository.findById(id).map(customerToDelete -> { + this.customerRepository.delete(customerToDelete); + return okSuccessResponse(customerToDelete); + }).orElseGet(ResponseFactory::notFoundErrorResponse); + } + + /* Tickets */ + @PostMapping("/{customerId}/screenings/{screeningId}") + public ResponseEntity bookTicket(@PathVariable (name = "customerId") int customerId, + @PathVariable (name = "screeningId") int screeningId, + @Valid @RequestBody Ticket ticket, BindingResult result) { + + if (result.hasErrors()) { + return badRequestErrorResponse(); + } + + Optional optionalCustomer = this.customerRepository.findById(customerId); + if (optionalCustomer.isEmpty()){ + return notFoundErrorResponse(); + } + + Optional optionalScreening = this.screeningRepository.findById(screeningId); + if (optionalScreening.isEmpty()){ + return notFoundErrorResponse(); + } + + Customer customer = optionalCustomer.get(); + Screening screening = optionalScreening.get(); + + ticket.setCustomer(customer); + ticket.setScreening(screening); + + Ticket savedTicket = this.ticketRepository.save(ticket); + return createdSuccessResponse(savedTicket); + + } + + @GetMapping("/{customerId}/screenings/{screeningId}") + public ResponseEntity getAllTickets(@PathVariable (name = "customerId") int customerId, + @PathVariable (name = "screeningId") int screeningId) { + + if (this.customerRepository.findById(customerId).isEmpty()) { + return notFoundErrorResponse(); + } + + if (this.screeningRepository.findById(screeningId).isEmpty()) { + return notFoundErrorResponse(); + } + + Customer customer = this.customerRepository.findById(customerId).get(); + Screening screening = this.screeningRepository.findById(screeningId).get(); + + List ticketList = this.ticketRepository.findAllByCustomerAndScreening(customer, screening); + + return okSuccessResponse(ticketList); + } + + + private void updateCustomerDetails(Customer oldCustomer, Customer newCustomer) { + oldCustomer.setName(newCustomer.getName()); + oldCustomer.setPhone(newCustomer.getPhone()); + oldCustomer.setEmail(newCustomer.getEmail()); + oldCustomer.setUpdatedAt(OffsetDateTime.now()); + } + + private CustomerResponseDTO convertToCustomerResponseDTO(Customer customer){ + return new CustomerResponseDTO(customer.getId(), customer.getName(), customer.getEmail(), customer.getPhone(), customer.getCreatedAt(), customer.getUpdatedAt()); + } + + +} diff --git a/src/main/java/com/booleanuk/api/cinema/customer/model/Customer.java b/src/main/java/com/booleanuk/api/cinema/customer/model/Customer.java new file mode 100644 index 00000000..1fdfee2b --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/customer/model/Customer.java @@ -0,0 +1,64 @@ +package com.booleanuk.api.cinema.customer.model; + +import com.booleanuk.api.cinema.ticket.model.Ticket; +import com.fasterxml.jackson.annotation.JsonManagedReference; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.List; + +@Getter +@Setter +@NoArgsConstructor + +@Entity +@Table(name = "customers") +public class Customer { + + public Customer (String name, String email, String phone) { + this.name = name; + this.email = email; + this.phone = phone; + this.tickets = new ArrayList<>(); + } + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private int id; + + @NotBlank(message = "name is required") + @Column(name = "name", nullable = false) + private String name; + + @NotBlank(message = "email is required") + @Column(name = "email", nullable = false) + private String email; + + @NotBlank(message = "phone is required") + @Column(name = "phone", nullable = false) + private String phone; + + @Column(name = "createdAt", nullable = false) + private OffsetDateTime createdAt; + + @Column(name = "updatedAt", nullable = false) + private OffsetDateTime updatedAt; + + @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonManagedReference(value = "customer-tickets") + List tickets; + + @PrePersist + private void onCreate() { + /* + This method is called before the entity manager saves the entity to the database. + */ + this.createdAt = OffsetDateTime.now(); + this.updatedAt = OffsetDateTime.now(); + } +} diff --git a/src/main/java/com/booleanuk/api/cinema/customer/model/CustomerResponseDTO.java b/src/main/java/com/booleanuk/api/cinema/customer/model/CustomerResponseDTO.java new file mode 100644 index 00000000..bf57fc83 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/customer/model/CustomerResponseDTO.java @@ -0,0 +1,35 @@ +package com.booleanuk.api.cinema.customer.model; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.OffsetDateTime; + +@Getter +@Setter +@NoArgsConstructor + +public class CustomerResponseDTO { + + public CustomerResponseDTO (int id, String name, String email, String phone, OffsetDateTime createdAt, OffsetDateTime updatedAt) { + this.id = id; + this.name = name; + this.email = email; + this.phone = phone; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + private int id; + + private String name; + + private String email; + + private String phone; + + private OffsetDateTime createdAt; + + private OffsetDateTime updatedAt; +} diff --git a/src/main/java/com/booleanuk/api/cinema/customer/repository/CustomerRepository.java b/src/main/java/com/booleanuk/api/cinema/customer/repository/CustomerRepository.java new file mode 100644 index 00000000..4d04e064 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/customer/repository/CustomerRepository.java @@ -0,0 +1,7 @@ +package com.booleanuk.api.cinema.customer.repository; + +import com.booleanuk.api.cinema.customer.model.Customer; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CustomerRepository extends JpaRepository { +} diff --git a/src/main/java/com/booleanuk/api/cinema/mapper/MovieMapper.java b/src/main/java/com/booleanuk/api/cinema/mapper/MovieMapper.java new file mode 100644 index 00000000..2922ad32 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/mapper/MovieMapper.java @@ -0,0 +1,19 @@ +package com.booleanuk.api.cinema.mapper; + +import com.booleanuk.api.cinema.movie.model.Movie; +import com.booleanuk.api.cinema.movie.model.MovieRequestDTO; +import com.booleanuk.api.cinema.movie.model.MovieResponseDTO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper(componentModel = "spring") +public interface MovieMapper { + + // Ignored the targets when mapping. + @Mapping(target = "id", ignore = true) + @Mapping(target = "createdAt", ignore = true) + @Mapping(target = "updatedAt", ignore = true) + Movie toEntity(MovieRequestDTO requestDTO); + + MovieResponseDTO toResponseDTO(Movie movie); +} diff --git a/src/main/java/com/booleanuk/api/cinema/movie/controller/MovieController.java b/src/main/java/com/booleanuk/api/cinema/movie/controller/MovieController.java new file mode 100644 index 00000000..28bb38d1 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/movie/controller/MovieController.java @@ -0,0 +1,152 @@ +package com.booleanuk.api.cinema.movie.controller; + +import com.booleanuk.api.cinema.mapper.MovieMapper; +import com.booleanuk.api.cinema.movie.model.Movie; +import com.booleanuk.api.cinema.movie.model.MovieRequestDTO; +import com.booleanuk.api.cinema.movie.model.MovieResponseDTO; +import com.booleanuk.api.cinema.movie.model.MovieUpdateDTO; +import com.booleanuk.api.cinema.movie.repository.MovieRepository; +import com.booleanuk.api.cinema.response.Response; +import com.booleanuk.api.cinema.response.ResponseFactory; +import com.booleanuk.api.cinema.screening.model.Screening; +import com.booleanuk.api.cinema.screening.repository.ScreeningRepository; +import jakarta.validation.Valid; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.*; + +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.List; + +import static com.booleanuk.api.cinema.response.ResponseFactory.*; + +@RestController +@RequestMapping("/movies") +public class MovieController { + + @Autowired + MovieRepository movieRepository; + + @Autowired + ScreeningRepository screeningRepository; + + @Autowired + MovieMapper movieMapper; + + @PostMapping + public ResponseEntity addMovie(@Valid @RequestBody MovieRequestDTO movieRequestDTO, BindingResult result) { + if (result.hasErrors()){ + return badRequestErrorResponse(); + } + + Movie movie = movieMapper.toEntity(movieRequestDTO); + + // Set movie to each screening. + if (movie.getScreenings() != null) { + movie.getScreenings().forEach(s -> s.setMovie(movie)); + } + + // Save movie + Movie savedMovie = this.movieRepository.save(movie); + + // Convert to DTO without screenings + MovieResponseDTO responseDTO = movieMapper.toResponseDTO(savedMovie); + + return createdSuccessResponse(responseDTO); + } + + + @GetMapping + public ResponseEntity getAllMovies() { + List responseList = new ArrayList<>(); + this.movieRepository.findAll().forEach(movie -> responseList.add(movieMapper.toResponseDTO(movie))); + return okSuccessResponse(responseList); + } + + + @PutMapping("/{id}") + public ResponseEntity updateMovie(@PathVariable (name = "id") int id, + @Valid @RequestBody MovieUpdateDTO updatedMovieDTO, + BindingResult result) { + + if (result.hasErrors()) { + return badRequestErrorResponse(); + } + + return this.movieRepository.findById(id) + .map(movieToUpdate -> { + updateMovieDetails(movieToUpdate, updatedMovieDTO); + Movie savedMovie = this.movieRepository.save(movieToUpdate); + MovieResponseDTO responseDTO = movieMapper.toResponseDTO(savedMovie); + return createdSuccessResponse(responseDTO); + }).orElseGet(ResponseFactory::notFoundErrorResponse); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteMovie(@PathVariable (name = "id") int id) { + return this.movieRepository.findById(id) + .map(movie -> { + this.movieRepository.delete(movie); + MovieResponseDTO responseDTO = movieMapper.toResponseDTO(movie); + return okSuccessResponse(responseDTO); + }) + .orElseGet(ResponseFactory::notFoundErrorResponse); + } + + /* Screenings */ + @PostMapping("/{id}/screenings") + public ResponseEntity addScreening(@PathVariable (name = "id") int id, + @Valid @RequestBody Screening screening, + BindingResult result) { + + if (result.hasErrors()) { + return badRequestErrorResponse(); + } + + return this.movieRepository.findById(id).map(movie -> { + screening.setMovie(movie); + return createdSuccessResponse(this.screeningRepository.save(screening)); + }).orElseGet(ResponseFactory::notFoundErrorResponse); + } + + @GetMapping("{id}/screenings") + public ResponseEntity getAllScreenings(@PathVariable (name = "id") int id) { + return this.movieRepository.findById(id) + .map(movie -> okSuccessResponse(this.screeningRepository.findScreeningsByMovie(movie))) + .orElseGet(ResponseFactory::notFoundErrorResponse); + } + + /* Helper functions */ + private void updateMovieDetails(Movie oldMovie, MovieUpdateDTO newMovie) { + if (newMovie.getTitle() != null) { + oldMovie.setTitle(newMovie.getTitle()); + } + + if (newMovie.getRating() != null) { + oldMovie.setRating(newMovie.getRating()); + } + + if (newMovie.getDescription() != null) { + oldMovie.setDescription(newMovie.getDescription()); + } + + if (newMovie.getRuntimeMins() != null) { + oldMovie.setRuntimeMins(newMovie.getRuntimeMins()); + } + + oldMovie.setUpdatedAt(OffsetDateTime.now()); + } + + private MovieResponseDTO convertToResponseDTO(Movie movie) { + return new MovieResponseDTO( + movie.getId(), + movie.getTitle(), + movie.getRating(), + movie.getDescription(), + movie.getRuntimeMins(), + movie.getCreatedAt(), + movie.getUpdatedAt()); + } +} diff --git a/src/main/java/com/booleanuk/api/cinema/movie/model/Movie.java b/src/main/java/com/booleanuk/api/cinema/movie/model/Movie.java new file mode 100644 index 00000000..35147ff0 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/movie/model/Movie.java @@ -0,0 +1,65 @@ +package com.booleanuk.api.cinema.movie.model; + +import com.booleanuk.api.cinema.screening.model.Screening; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonManagedReference; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.OffsetDateTime; +import java.util.List; + +@Getter +@Setter +@NoArgsConstructor + +@Entity +@Table(name = "movies") +public class Movie { + public Movie (String title, String rating, String description, Integer runtimeMins, List screenings) { + this.title = title; + this.rating = rating; + this.description = description; + this.runtimeMins = runtimeMins; + this.screenings = screenings; + } + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private int id; + + @Column(name = "title", nullable = false) + private String title; + + @Column(name = "rating", nullable = false) + private String rating; + + @Column(name = "description", nullable = false) + private String description; + + @Column(name = "runtimeMins", nullable = false) + private Integer runtimeMins; + + @Column(name = "createdAt", nullable = false) + private OffsetDateTime createdAt; + + @Column(name = "updatedAt", nullable = false) + private OffsetDateTime updatedAt; + + @OneToMany(mappedBy = "movie", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonManagedReference + private List screenings; + + @PrePersist + private void onCreate() { + /* + This method is called before the entity manager saves the entity to the database. + */ + this.createdAt = OffsetDateTime.now(); + this.updatedAt = OffsetDateTime.now(); + } +} diff --git a/src/main/java/com/booleanuk/api/cinema/movie/model/MovieRequestDTO.java b/src/main/java/com/booleanuk/api/cinema/movie/model/MovieRequestDTO.java new file mode 100644 index 00000000..12983a09 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/movie/model/MovieRequestDTO.java @@ -0,0 +1,51 @@ +package com.booleanuk.api.cinema.movie.model; + +import com.booleanuk.api.cinema.screening.model.Screening; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.List; + +@Getter +@Setter +@NoArgsConstructor + +public class MovieRequestDTO { + + public MovieRequestDTO(String title, String rating, String description, Integer runtimeMins) { + this.title = title; + this.rating = rating; + this.description = description; + this.runtimeMins = runtimeMins; + this.screenings = new ArrayList<>(); + } + + public MovieRequestDTO(String title, String rating, String description, Integer runtimeMins, List screenings) { + this.title = title; + this.rating = rating; + this.description = description; + this.runtimeMins = runtimeMins; + this.screenings = screenings; + } + + // Bean validation framework. Validates object fields at application level. + @NotBlank(message = "Title is required") + private String title; + + @NotBlank(message = "Rating is required") + private String rating; + + @NotBlank(message = "Description is required") + private String description; + + // Validation check for non-string fields. + @NotNull(message = "RuntimeMins is required") + private Integer runtimeMins; + + // Optional + private List screenings; +} diff --git a/src/main/java/com/booleanuk/api/cinema/movie/model/MovieResponseDTO.java b/src/main/java/com/booleanuk/api/cinema/movie/model/MovieResponseDTO.java new file mode 100644 index 00000000..657b0945 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/movie/model/MovieResponseDTO.java @@ -0,0 +1,38 @@ +package com.booleanuk.api.cinema.movie.model; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.OffsetDateTime; + +@Getter +@Setter +@NoArgsConstructor + +public class MovieResponseDTO { + + public MovieResponseDTO(int id, String title, String rating, String description, Integer runtimeMins, OffsetDateTime createdAt, OffsetDateTime updatedAt) { + this.id = id; + this.title = title; + this.rating = rating; + this.description = description; + this.runtimeMins = runtimeMins; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + private int id; + + private String title; + + private String rating; + + private String description; + + private Integer runtimeMins; + + private OffsetDateTime createdAt; + + private OffsetDateTime updatedAt; +} diff --git a/src/main/java/com/booleanuk/api/cinema/movie/model/MovieUpdateDTO.java b/src/main/java/com/booleanuk/api/cinema/movie/model/MovieUpdateDTO.java new file mode 100644 index 00000000..b2ca2ad5 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/movie/model/MovieUpdateDTO.java @@ -0,0 +1,26 @@ +package com.booleanuk.api.cinema.movie.model; + +import com.booleanuk.api.cinema.screening.model.Screening; +import lombok.Data; + +import java.time.OffsetDateTime; +import java.util.List; + +@Data + +public class MovieUpdateDTO { + + private String title; + + private String rating; + + private String description; + + private Integer runtimeMins; + + private OffsetDateTime createdAt; + + private OffsetDateTime updatedAt; + + private List screenings; +} diff --git a/src/main/java/com/booleanuk/api/cinema/movie/repository/MovieRepository.java b/src/main/java/com/booleanuk/api/cinema/movie/repository/MovieRepository.java new file mode 100644 index 00000000..55446633 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/movie/repository/MovieRepository.java @@ -0,0 +1,7 @@ +package com.booleanuk.api.cinema.movie.repository; + +import com.booleanuk.api.cinema.movie.model.Movie; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MovieRepository extends JpaRepository { +} diff --git a/src/main/java/com/booleanuk/api/cinema/response/ErrorResponse.java b/src/main/java/com/booleanuk/api/cinema/response/ErrorResponse.java new file mode 100644 index 00000000..51864196 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/response/ErrorResponse.java @@ -0,0 +1,25 @@ +package com.booleanuk.api.cinema.response; + +import java.util.HashMap; +import java.util.Map; + +public class ErrorResponse implements Response { + private final String status; + private final Map data; + + public ErrorResponse(String message) { + this.status = "error"; + this.data = new HashMap<>(); + this.data.put("message", message); + } + + @Override + public String getStatus() { + return this.status; + } + + @Override + public Object getData() { + return this.data; + } +} diff --git a/src/main/java/com/booleanuk/api/cinema/response/Response.java b/src/main/java/com/booleanuk/api/cinema/response/Response.java new file mode 100644 index 00000000..41671b50 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/response/Response.java @@ -0,0 +1,6 @@ +package com.booleanuk.api.cinema.response; + +public interface Response { + String getStatus(); + Object getData(); +} diff --git a/src/main/java/com/booleanuk/api/cinema/response/ResponseFactory.java b/src/main/java/com/booleanuk/api/cinema/response/ResponseFactory.java new file mode 100644 index 00000000..138319c4 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/response/ResponseFactory.java @@ -0,0 +1,37 @@ +package com.booleanuk.api.cinema.response; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +public class ResponseFactory { + + + // Utility classes should not have a public constructor. + private ResponseFactory(){ + throw new IllegalStateException("utility class"); + } + + // Static method to create an HttpStatus OK response + public static ResponseEntity okSuccessResponse(T data) { + SuccessResponse successResponse = new SuccessResponse<>(data); + return new ResponseEntity<>(successResponse, HttpStatus.OK); + } + + // Static method to create an HttpStatus CREATED response + public static ResponseEntity createdSuccessResponse(T data) { + SuccessResponse successResponse = new SuccessResponse<>(data); + return new ResponseEntity<>(successResponse, HttpStatus.CREATED); + } + + // Static method to create a Bad Request response + public static ResponseEntity badRequestErrorResponse() { + ErrorResponse errorResponse = new ErrorResponse("bad request"); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + // Static method to create a Not Found response + public static ResponseEntity notFoundErrorResponse() { + ErrorResponse errorResponse = new ErrorResponse("not found"); + return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND); + } +} diff --git a/src/main/java/com/booleanuk/api/cinema/response/SuccessResponse.java b/src/main/java/com/booleanuk/api/cinema/response/SuccessResponse.java new file mode 100644 index 00000000..847ac077 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/response/SuccessResponse.java @@ -0,0 +1,26 @@ +package com.booleanuk.api.cinema.response; + +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +public class SuccessResponse implements Response { + private String status; + private T data; + + public SuccessResponse(T data) { + this.status = "success"; + this.data = data; + } + + @Override + public String getStatus() { + return status; + } + + @Override + public T getData(){ + return data; + } +} diff --git a/src/main/java/com/booleanuk/api/cinema/screening/model/Screening.java b/src/main/java/com/booleanuk/api/cinema/screening/model/Screening.java new file mode 100644 index 00000000..f36ebf10 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/screening/model/Screening.java @@ -0,0 +1,65 @@ +package com.booleanuk.api.cinema.screening.model; + +import com.booleanuk.api.cinema.movie.model.Movie; +import com.fasterxml.jackson.annotation.JsonBackReference; +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.LocalDateTime; +import java.time.OffsetDateTime; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor + +@Entity +@Table(name = "screenings") +public class Screening { + public Screening (int screenNumber, int capacity, OffsetDateTime startsAt) { + this.screenNumber = screenNumber; + this.capacity = capacity; + this.startsAt = startsAt; + } + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private int id; + + @NotNull(message = "screenNumber is required") + @Column(name = "screenNumber", nullable = false) + private Integer screenNumber; + + @NotNull(message = "startsAt is required") + @Column(name = "startsAt", nullable = false) + private OffsetDateTime startsAt; + + @NotNull(message = "capacity is required") + @Column(name = "capacity", nullable = false) + private Integer capacity; + + @Column(name = "createdAt", nullable = false) + private LocalDateTime createdAt; + + @Column(name = "updatedAt", nullable = false) + private LocalDateTime updatedAt; + + @ManyToOne + @JoinColumn(name = "movieId", nullable = false) + @JsonBackReference + private Movie movie; + + @PrePersist + private void onCreate() { + /* + This method is called before the entity manager saves the entity to the database. + */ + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + } +} diff --git a/src/main/java/com/booleanuk/api/cinema/screening/repository/ScreeningRepository.java b/src/main/java/com/booleanuk/api/cinema/screening/repository/ScreeningRepository.java new file mode 100644 index 00000000..ee42c806 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/screening/repository/ScreeningRepository.java @@ -0,0 +1,11 @@ +package com.booleanuk.api.cinema.screening.repository; + +import com.booleanuk.api.cinema.movie.model.Movie; +import com.booleanuk.api.cinema.screening.model.Screening; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface ScreeningRepository extends JpaRepository { + public List findScreeningsByMovie(Movie movie); +} diff --git a/src/main/java/com/booleanuk/api/cinema/ticket/model/Ticket.java b/src/main/java/com/booleanuk/api/cinema/ticket/model/Ticket.java new file mode 100644 index 00000000..454046c9 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/ticket/model/Ticket.java @@ -0,0 +1,53 @@ +package com.booleanuk.api.cinema.ticket.model; + +import com.booleanuk.api.cinema.customer.model.Customer; +import com.booleanuk.api.cinema.screening.model.Screening; +import com.fasterxml.jackson.annotation.JsonBackReference; +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import lombok.*; + +import java.time.LocalDateTime; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor + +@Entity +@Table(name = "tickets") +public class Ticket { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private int id; + + @NotNull(message = "numberOfSeats is required") + @Column(name = "numberOfSeats") + private int numberOfSeats; + + @Column(name = "createdAt") + private LocalDateTime createdAt; + + @Column(name = "updatedAt") + private LocalDateTime updatedAt; + + @ManyToOne + @JoinColumn(name = "customerId", nullable = false) + @JsonBackReference(value = "customer-tickets") + private Customer customer; + + @ManyToOne + @JoinColumn(name = "screeningId", nullable = false) + @JsonBackReference(value = "movie tickets") + private Screening screening; + + @PrePersist + private void onCreate() { + /* + This method is called before the entity manager saves the entity to the database. + */ + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + } +} diff --git a/src/main/java/com/booleanuk/api/cinema/ticket/repository/TicketRepository.java b/src/main/java/com/booleanuk/api/cinema/ticket/repository/TicketRepository.java new file mode 100644 index 00000000..7ae4d968 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/ticket/repository/TicketRepository.java @@ -0,0 +1,12 @@ +package com.booleanuk.api.cinema.ticket.repository; + +import com.booleanuk.api.cinema.customer.model.Customer; +import com.booleanuk.api.cinema.screening.model.Screening; +import com.booleanuk.api.cinema.ticket.model.Ticket; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface TicketRepository extends JpaRepository { + public List findAllByCustomerAndScreening(Customer customer, Screening screening); +} diff --git a/src/main/resources/application.yml.example b/src/main/resources/application.yml.example deleted file mode 100644 index 275ec30f..00000000 --- a/src/main/resources/application.yml.example +++ /dev/null @@ -1,23 +0,0 @@ -server: - port: 4000 - error: - include-message: always - include-binding-errors: always - include-stacktrace: never - include-exception: false - -spring: - datasource: - url: jdbc:postgresql://DATABASE_URL:5432/DATABASE_NAME - username: DATABASE_USERNAME - password: DATABASE_PASSWORD - max-active: 3 - max-idle: 3 - jpa: - hibernate: - ddl-auto: update - properties: - hibernate: - dialect: org.hibernate.dialect.PostgreSQLDialect - format_sql: true - show-sql: true