From d9a164ef605ba3c13e7bf9d9b1df1a1ac2ef11a6 Mon Sep 17 00:00:00 2001 From: Muhammad Surani <ms02309@surrey.ac.uk> Date: Wed, 27 Mar 2024 22:52:40 +0000 Subject: [PATCH] Implementation of the Flight Booking Service --- BookingService/.gitignore | 263 ++++++++++++ .../FlightBooking.Service.Tests.csproj | 37 ++ .../FlightBooking.Service.Tests/UnitTest1.cs | 10 + BookingService/FlightBooking.Service.sln | 31 ++ .../Controllers/BookingsController.cs | 88 ++++ .../Controllers/SeatsController.cs | 42 ++ .../Controllers/StripeController.cs | 69 ++++ .../Data/Configs/ConfigSettings.cs | 15 + .../Data/Configs/ConfigSettingsModule.cs | 10 + .../FlightBooking.Service/Data/Constants.cs | 38 ++ .../Data/DTO/BookingDTO.cs | 56 +++ .../Data/DTO/BookingOrderDTO.cs | 25 ++ .../Data/DTO/FlightFareDTO.cs | 16 + .../Data/DTO/FlightInformationDTO.cs | 19 + .../Data/DTO/ReservedSeatDTO.cs | 17 + .../Data/DTO/StripeDataDTO.cs | 32 ++ .../Data/DatabaseSeeding.cs | 158 +++++++ .../FlightBooking.Service/Data/Enums.cs | 40 ++ .../Data/FlightBookingContext.cs | 70 ++++ .../BookingConfiguration.cs | 27 ++ .../BookingOrderConfiguration.cs | 13 + .../FlightFareConfiguration.cs | 17 + .../FlightInformationConfiguration.cs | 13 + .../PaymentConfiguration.cs | 17 + .../ReservedSeatConfiguration.cs | 17 + .../Data/Models/Booking.cs | 36 ++ .../Data/Models/BookingOrder.cs | 26 ++ .../Data/Models/FlightFare.cs | 28 ++ .../Data/Models/FlightInformation.cs | 25 ++ .../Data/Models/PassengerInformation.cs | 20 + .../Data/Models/Payment.cs | 30 ++ .../Data/Models/ReservedSeat.cs | 22 + .../Data/Repository/GenericRepository.cs | 257 ++++++++++++ .../Data/Repository/IGenericRepository.cs | 36 ++ .../Data/Repository/RepositoryModule.cs | 17 + .../FlightBooking.Service.csproj | 45 ++ .../FlightBooking.Service.http | 6 + .../Middleware/ErrorHandlingMiddleware.cs | 6 + .../Middleware/MiddlewareExtensions.cs | 10 + .../FlightBooking.Service/Program.cs | 63 +++ .../Properties/launchSettings.json | 41 ++ .../Services/BookingOrderService.cs | 391 ++++++++++++++++++ .../Services/BookingService.cs | 101 +++++ .../Services/FlightBookingProfile.cs | 29 ++ .../Services/FlightFareService.cs | 44 ++ .../Services/FlightService.cs | 47 +++ .../Interfaces/IBookingOrderService.cs | 21 + .../Services/Interfaces/IBookingService.cs | 35 ++ .../Services/Interfaces/IFlightFareService.cs | 21 + .../Services/Interfaces/IFlightService.cs | 22 + .../Interfaces/IReservedSeatService.cs | 29 ++ .../Services/Interfaces/IStripeService.cs | 22 + .../Services/PaymentService.cs | 6 + .../Services/ReservedSeatService.cs | 127 ++++++ .../Services/ResponseFormatter.cs | 176 ++++++++ .../Services/ServicesModule.cs | 17 + .../Services/StripeService.cs | 147 +++++++ .../FlightBooking.Service/Startup.cs | 162 ++++++++ .../appsettings.Development.json | 21 + .../FlightBooking.Service/appsettings.json | 22 + .../FlightBooking.Service/nlog.config | 40 ++ .../FlightBooking.Service/readme.md | 385 +++++++++++++++++ 62 files changed, 3673 insertions(+) create mode 100644 BookingService/.gitignore create mode 100644 BookingService/FlightBooking.Service.Tests/FlightBooking.Service.Tests.csproj create mode 100644 BookingService/FlightBooking.Service.Tests/UnitTest1.cs create mode 100644 BookingService/FlightBooking.Service.sln create mode 100644 BookingService/FlightBooking.Service/Controllers/BookingsController.cs create mode 100644 BookingService/FlightBooking.Service/Controllers/SeatsController.cs create mode 100644 BookingService/FlightBooking.Service/Controllers/StripeController.cs create mode 100644 BookingService/FlightBooking.Service/Data/Configs/ConfigSettings.cs create mode 100644 BookingService/FlightBooking.Service/Data/Configs/ConfigSettingsModule.cs create mode 100644 BookingService/FlightBooking.Service/Data/Constants.cs create mode 100644 BookingService/FlightBooking.Service/Data/DTO/BookingDTO.cs create mode 100644 BookingService/FlightBooking.Service/Data/DTO/BookingOrderDTO.cs create mode 100644 BookingService/FlightBooking.Service/Data/DTO/FlightFareDTO.cs create mode 100644 BookingService/FlightBooking.Service/Data/DTO/FlightInformationDTO.cs create mode 100644 BookingService/FlightBooking.Service/Data/DTO/ReservedSeatDTO.cs create mode 100644 BookingService/FlightBooking.Service/Data/DTO/StripeDataDTO.cs create mode 100644 BookingService/FlightBooking.Service/Data/DatabaseSeeding.cs create mode 100644 BookingService/FlightBooking.Service/Data/Enums.cs create mode 100644 BookingService/FlightBooking.Service/Data/FlightBookingContext.cs create mode 100644 BookingService/FlightBooking.Service/Data/ModelConfigurations/BookingConfiguration.cs create mode 100644 BookingService/FlightBooking.Service/Data/ModelConfigurations/BookingOrderConfiguration.cs create mode 100644 BookingService/FlightBooking.Service/Data/ModelConfigurations/FlightFareConfiguration.cs create mode 100644 BookingService/FlightBooking.Service/Data/ModelConfigurations/FlightInformationConfiguration.cs create mode 100644 BookingService/FlightBooking.Service/Data/ModelConfigurations/PaymentConfiguration.cs create mode 100644 BookingService/FlightBooking.Service/Data/ModelConfigurations/ReservedSeatConfiguration.cs create mode 100644 BookingService/FlightBooking.Service/Data/Models/Booking.cs create mode 100644 BookingService/FlightBooking.Service/Data/Models/BookingOrder.cs create mode 100644 BookingService/FlightBooking.Service/Data/Models/FlightFare.cs create mode 100644 BookingService/FlightBooking.Service/Data/Models/FlightInformation.cs create mode 100644 BookingService/FlightBooking.Service/Data/Models/PassengerInformation.cs create mode 100644 BookingService/FlightBooking.Service/Data/Models/Payment.cs create mode 100644 BookingService/FlightBooking.Service/Data/Models/ReservedSeat.cs create mode 100644 BookingService/FlightBooking.Service/Data/Repository/GenericRepository.cs create mode 100644 BookingService/FlightBooking.Service/Data/Repository/IGenericRepository.cs create mode 100644 BookingService/FlightBooking.Service/Data/Repository/RepositoryModule.cs create mode 100644 BookingService/FlightBooking.Service/FlightBooking.Service.csproj create mode 100644 BookingService/FlightBooking.Service/FlightBooking.Service.http create mode 100644 BookingService/FlightBooking.Service/Middleware/ErrorHandlingMiddleware.cs create mode 100644 BookingService/FlightBooking.Service/Middleware/MiddlewareExtensions.cs create mode 100644 BookingService/FlightBooking.Service/Program.cs create mode 100644 BookingService/FlightBooking.Service/Properties/launchSettings.json create mode 100644 BookingService/FlightBooking.Service/Services/BookingOrderService.cs create mode 100644 BookingService/FlightBooking.Service/Services/BookingService.cs create mode 100644 BookingService/FlightBooking.Service/Services/FlightBookingProfile.cs create mode 100644 BookingService/FlightBooking.Service/Services/FlightFareService.cs create mode 100644 BookingService/FlightBooking.Service/Services/FlightService.cs create mode 100644 BookingService/FlightBooking.Service/Services/Interfaces/IBookingOrderService.cs create mode 100644 BookingService/FlightBooking.Service/Services/Interfaces/IBookingService.cs create mode 100644 BookingService/FlightBooking.Service/Services/Interfaces/IFlightFareService.cs create mode 100644 BookingService/FlightBooking.Service/Services/Interfaces/IFlightService.cs create mode 100644 BookingService/FlightBooking.Service/Services/Interfaces/IReservedSeatService.cs create mode 100644 BookingService/FlightBooking.Service/Services/Interfaces/IStripeService.cs create mode 100644 BookingService/FlightBooking.Service/Services/PaymentService.cs create mode 100644 BookingService/FlightBooking.Service/Services/ReservedSeatService.cs create mode 100644 BookingService/FlightBooking.Service/Services/ResponseFormatter.cs create mode 100644 BookingService/FlightBooking.Service/Services/ServicesModule.cs create mode 100644 BookingService/FlightBooking.Service/Services/StripeService.cs create mode 100644 BookingService/FlightBooking.Service/Startup.cs create mode 100644 BookingService/FlightBooking.Service/appsettings.Development.json create mode 100644 BookingService/FlightBooking.Service/appsettings.json create mode 100644 BookingService/FlightBooking.Service/nlog.config create mode 100644 BookingService/FlightBooking.Service/readme.md diff --git a/BookingService/.gitignore b/BookingService/.gitignore new file mode 100644 index 0000000..1242aad --- /dev/null +++ b/BookingService/.gitignore @@ -0,0 +1,263 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +#*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc +/WellaHealthApiCore/Generated +/WellahealthCore.AzureFunctions diff --git a/BookingService/FlightBooking.Service.Tests/FlightBooking.Service.Tests.csproj b/BookingService/FlightBooking.Service.Tests/FlightBooking.Service.Tests.csproj new file mode 100644 index 0000000..78b328a --- /dev/null +++ b/BookingService/FlightBooking.Service.Tests/FlightBooking.Service.Tests.csproj @@ -0,0 +1,37 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net8.0</TargetFramework> + <ImplicitUsings>enable</ImplicitUsings> + <Nullable>enable</Nullable> + + <IsPackable>false</IsPackable> + <IsTestProject>true</IsTestProject> + </PropertyGroup> + <PropertyGroup> + <EnableNETAnalyzers>true</EnableNETAnalyzers> + </PropertyGroup> + <ItemGroup> + <PackageReference Include="coverlet.collector" Version="6.0.1"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" /> + <PackageReference Include="xunit" Version="2.5.3" /> + <PackageReference Include="xunit.runner.visualstudio" Version="2.5.7"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + <PackageReference Include="MockQueryable.Moq" Version="7.0.0" /> + <PackageReference Include="Moq" Version="4.18.4" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\FlightBooking.Service\FlightBooking.Service.csproj" /> + </ItemGroup> + + <ItemGroup> + <Using Include="Xunit" /> + </ItemGroup> + +</Project> diff --git a/BookingService/FlightBooking.Service.Tests/UnitTest1.cs b/BookingService/FlightBooking.Service.Tests/UnitTest1.cs new file mode 100644 index 0000000..127caef --- /dev/null +++ b/BookingService/FlightBooking.Service.Tests/UnitTest1.cs @@ -0,0 +1,10 @@ +namespace FlightBooking.Service.Tests +{ + public class UnitTest1 + { + [Fact] + public void Test1() + { + } + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service.sln b/BookingService/FlightBooking.Service.sln new file mode 100644 index 0000000..2a079f3 --- /dev/null +++ b/BookingService/FlightBooking.Service.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34622.214 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FlightBooking.Service", "FlightBooking.Service\FlightBooking.Service.csproj", "{0043031F-8453-4554-90A8-DA1921F95235}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FlightBooking.Service.Tests", "FlightBooking.Service.Tests\FlightBooking.Service.Tests.csproj", "{C7FCD62C-5D7D-44BB-8A38-821BEBE2BBC9}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0043031F-8453-4554-90A8-DA1921F95235}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0043031F-8453-4554-90A8-DA1921F95235}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0043031F-8453-4554-90A8-DA1921F95235}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0043031F-8453-4554-90A8-DA1921F95235}.Release|Any CPU.Build.0 = Release|Any CPU + {C7FCD62C-5D7D-44BB-8A38-821BEBE2BBC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C7FCD62C-5D7D-44BB-8A38-821BEBE2BBC9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C7FCD62C-5D7D-44BB-8A38-821BEBE2BBC9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C7FCD62C-5D7D-44BB-8A38-821BEBE2BBC9}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {1CCB1631-8F04-4C14-AC4D-9CFC54B609B7} + EndGlobalSection +EndGlobal diff --git a/BookingService/FlightBooking.Service/Controllers/BookingsController.cs b/BookingService/FlightBooking.Service/Controllers/BookingsController.cs new file mode 100644 index 0000000..d9d2de4 --- /dev/null +++ b/BookingService/FlightBooking.Service/Controllers/BookingsController.cs @@ -0,0 +1,88 @@ +using FlightBooking.Service.Data.DTO; +using FlightBooking.Service.Services; +using FlightBooking.Service.Services.Interfaces; +using Microsoft.AspNetCore.Mvc; +using System.Net.Mime; + +namespace FlightBooking.Service.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class BookingsController : ControllerBase + { + private readonly IBookingService _bookingService; + private readonly IBookingOrderService _bookingOrderService; + + public BookingsController(IBookingService bookingService, IBookingOrderService bookingOrderService) + { + _bookingService = bookingService; + _bookingOrderService = bookingOrderService; + } + + [HttpGet("bookingNumber/{bookingNumber}")] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(BookingDTO))] + [ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ProblemDetails))] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ProblemDetails))] + public async Task<IActionResult> GetBookingsByBookingNumber([FromRoute] string bookingNumber) + { + ServiceResponse<BookingDTO?> result = await _bookingService.GetBookingByBookingNumberAsync(bookingNumber); + + return result.FormatResponse(); + } + + [HttpGet("email/{email}")] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<BookingDTO>))] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ProblemDetails))] + public IActionResult GetBookingsByEmail([FromRoute] string email) + { + ServiceResponse<IEnumerable<BookingDTO>?> result = _bookingService.GetBookingsByEmail(email); + + return result.FormatResponse(); + } + + [HttpGet("orderNumber/{orderNumber}")] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<BookingDTO>))] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ProblemDetails))] + public IActionResult GetBookingsByOrderNumber([FromRoute] string orderNumber) + { + ServiceResponse<IEnumerable<BookingDTO>?> result = _bookingService.GetBookingsByOrderNumber(orderNumber); + + return result.FormatResponse(); + } + + [HttpGet("id/{bookingId}")] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(BookingDTO))] + [ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ProblemDetails))] + public async Task<IActionResult> GetBookingById([FromRoute] int bookingId) + { + ServiceResponse<BookingDTO?> result = await _bookingService.GetBookingByBookingId(bookingId); + + return result.FormatResponse(); + } + + [HttpPost] + [Consumes(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(BookingResponseDTO))] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity, Type = typeof(ProblemDetails))] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ProblemDetails))] + [ProducesResponseType(StatusCodes.Status500InternalServerError, Type = typeof(ProblemDetails))] + public async Task<IActionResult> PostBooking([FromBody] BookingOrderDTO bookingOrderDTO) + { + ServiceResponse<BookingResponseDTO?> result = await _bookingOrderService.CreateBookingOrderAsync(bookingOrderDTO); + + return result.FormatResponse(); + } + + [HttpGet("payment/{orderNumber}")] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(BookingResponseDTO))] + [ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ProblemDetails))] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity, Type = typeof(ProblemDetails))] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ProblemDetails))] + public async Task<IActionResult> GetBookingPayment([FromRoute] string orderNumber) + { + ServiceResponse<BookingResponseDTO?> result = await _bookingOrderService.GetCheckoutUrlAsync(orderNumber); + + return result.FormatResponse(); + } + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Controllers/SeatsController.cs b/BookingService/FlightBooking.Service/Controllers/SeatsController.cs new file mode 100644 index 0000000..a6ca8bb --- /dev/null +++ b/BookingService/FlightBooking.Service/Controllers/SeatsController.cs @@ -0,0 +1,42 @@ +using FlightBooking.Service.Data.DTO; +using FlightBooking.Service.Services; +using FlightBooking.Service.Services.Interfaces; +using Microsoft.AspNetCore.Mvc; +using System.Net.Mime; + +namespace FlightBooking.Service.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class SeatsController : ControllerBase + { + private readonly IReservedSeatService _service; + + public SeatsController(IReservedSeatService service) + { + _service = service; + } + + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<ReservedSeatDTO>))] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ProblemDetails))] + public IActionResult GetAvailableSeats([FromQuery] string flightNumber) + { + ServiceResponse<IEnumerable<ReservedSeatDTO>> result = _service.GetAvailableSeatsByFlightNumber(flightNumber); + + return result.FormatResponse(); + } + + [HttpPost] + [Consumes(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity, Type = typeof(ProblemDetails))] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ProblemDetails))] + public async Task<IActionResult> ReserveSeat([FromBody] ReservedSeatRequestDTO requestDTO) + { + ServiceResponse<string> result = await _service.ReserveSeatAsync(requestDTO); + + return result.FormatResponse(); + } + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Controllers/StripeController.cs b/BookingService/FlightBooking.Service/Controllers/StripeController.cs new file mode 100644 index 0000000..0f9bf55 --- /dev/null +++ b/BookingService/FlightBooking.Service/Controllers/StripeController.cs @@ -0,0 +1,69 @@ +using FlightBooking.Service.Data.Configs; +using FlightBooking.Service.Services; +using FlightBooking.Service.Services.Interfaces; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Stripe; +using System.Text; + +namespace FlightBooking.Service.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class StripeController : ControllerBase + { + private readonly StripeConfig _stripeConfig; + private readonly IStripeService _stripeService; + private readonly ILogger<StripeController> _logger; + + public StripeController(IOptionsMonitor<StripeConfig> options, IStripeService stripeService, ILogger<StripeController> logger) + { + _stripeConfig = options.CurrentValue; + _stripeService = stripeService; + _logger = logger; + } + + //webhook for stripe to verify payment + [HttpPost] + public async Task<IActionResult> NotificationWebhookAsync() + { + //var json = await new StreamReader(HttpContext.Request.Body).ReadToEndAsync(); + + string requestBody; + using (StreamReader reader = new StreamReader(Request.Body, Encoding.UTF8)) + { + requestBody = await reader.ReadToEndAsync(); + } + + if (string.IsNullOrEmpty(requestBody)) + { + //_logger.LogInformation("Event object is empty", eventObject); + return BadRequest(); + } + + string stripeSigningKey = _stripeConfig.SigningSecret; + + Event stripeEvent; + + try + { + stripeEvent = EventUtility.ConstructEvent(requestBody, Request.Headers["Stripe-Signature"], stripeSigningKey, throwOnApiVersionMismatch: false); + } + catch (StripeException exception) + { + _logger.LogError(exception.ToString()); + return BadRequest(); + } + + //Since this is the only event we are handling. + if (stripeEvent.Type == Events.CheckoutSessionCompleted) + { + var response = await _stripeService.ProcessPayment(stripeEvent); + + return response.FormatResponse(); + } + + return Ok(); + } + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Data/Configs/ConfigSettings.cs b/BookingService/FlightBooking.Service/Data/Configs/ConfigSettings.cs new file mode 100644 index 0000000..72ff348 --- /dev/null +++ b/BookingService/FlightBooking.Service/Data/Configs/ConfigSettings.cs @@ -0,0 +1,15 @@ +namespace FlightBooking.Service.Data.Configs +{ + public class ConfigSettings + { + } + + public class StripeConfig + { + public const string ConfigName = nameof(StripeConfig); + + public string SecretKey { get; set; } = null!; + public string PublicKey { get; set; } = null!; + public string SigningSecret { get; set; } = null!; + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Data/Configs/ConfigSettingsModule.cs b/BookingService/FlightBooking.Service/Data/Configs/ConfigSettingsModule.cs new file mode 100644 index 0000000..21c0d09 --- /dev/null +++ b/BookingService/FlightBooking.Service/Data/Configs/ConfigSettingsModule.cs @@ -0,0 +1,10 @@ +namespace FlightBooking.Service.Data.Configs +{ + public static class ConfigSettingsModule + { + public static void AddConfigSettings(this IServiceCollection services, IConfiguration configuration) + { + services.Configure<StripeConfig>(configuration.GetSection(StripeConfig.ConfigName)); + } + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Data/Constants.cs b/BookingService/FlightBooking.Service/Data/Constants.cs new file mode 100644 index 0000000..ff8ff81 --- /dev/null +++ b/BookingService/FlightBooking.Service/Data/Constants.cs @@ -0,0 +1,38 @@ +namespace FlightBooking.Service.Data +{ + public class Constants + { + } + + internal static class RepositoryConstants + { + public const string LoggingStarted = "Started logging"; + public const string CreateNullError = "Attempt to insert empty entity. Type of Entity : {0}"; + public const string DeleteNullError = "Could not find entity for deleting. type of Entity : {0}"; + public const string BulkDeleteNullError = "Attempt to Delete empty list of entities. Type of Entity : {0}"; + public const string BulkCreateNullError = "Attempt to insert empty list of entities. Type of Entity : {0}"; + public const string EmptySaveInfo = "No changes was written to underlying database."; + public const string UpdateException = "Update Exception"; + public const string UpdateConcurrencyException = "Update Concurrency Exception"; + public const string SaveChangesException = "Generic Error in Generic Repo Update method"; + } + + internal static class ServiceErrorMessages + { + public const string Success = "The operation was successful"; + public const string Failed = "An unhandled errror has occured while processing your request"; + public const string UpdateError = "There was an error carrying out operation"; + public const string MisMatch = "The entity Id does not match the supplied Id"; + public const string EntityIsNull = "Supplied entity is null or supplied list of entities is empty. Check our docs"; + public const string EntityNotFound = "The requested resource was not found. Verify that the supplied Id is correct"; + public const string Incompleted = "Some actions may not have been successfully processed"; + public const string EntityExist = "An entity of the same name or id exists"; + public const string InvalidParam = "A supplied parameter or model is invalid. Check the docs"; + public const string UnprocessableEntity = "The action cannot be processed"; + public const string InternalServerError = "An internal server error and request could not processed"; + public const string OperationFailed = "Operation failed"; + + public const string ParameterEmptyOrNull = "The parameter list is null or empty"; + public const string RequestIdRequired = "Request Id is required"; + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Data/DTO/BookingDTO.cs b/BookingService/FlightBooking.Service/Data/DTO/BookingDTO.cs new file mode 100644 index 0000000..8a93333 --- /dev/null +++ b/BookingService/FlightBooking.Service/Data/DTO/BookingDTO.cs @@ -0,0 +1,56 @@ +using System.ComponentModel.DataAnnotations; + +namespace FlightBooking.Service.Data.DTO +{ + public class BookingDTO + { + public int Id { get; set; } + public string FirstName { get; set; } = null!; + public string LastName { get; set; } = null!; + public string PhoneNumber { get; set; } = null!; + public string Email { get; set; } = null!; + public string Address { get; set; } = null!; + public DateOnly DateOfBirth { get; set; } + + [EnumDataType(typeof(Gender))] + public Gender Gender { get; set; } + public DateTime CreatedAt { get; set; } + + public string BookingNumber { get; set; } = null!; + public int BookingOrderId { get; set; } + + [EnumDataType(typeof(BookingStatus))] + public BookingStatus BookingStatus { get; set; } = BookingStatus.Pending; + public string? SeatNumber { get; set; } + + public BookingFlightInformationDTO FlightInformation { get; set; } = null!; + public BookingFlightFareDTO FlightFare { get; set; } = null!; + } + + public class BookingRequestDTO + { + [Required] + public string FirstName { get; set; } = null!; + + [Required] + public string LastName { get; set; } = null!; + + public string PhoneNumber { get; set; } = null!; + public string? Email { get; set; } + + [Required] + public string Address { get; set; } = null!; + + [Required] + public DateOnly DateOfBirth { get; set; } + + [EnumDataType(typeof(Gender))] + public Gender Gender { get; set; } = Gender.PreferNotToSay; + + [Required] + [Range(1, int.MaxValue)] + public int OutboundFareId { get; set; } //We are keeping Fare per booking to allow flexibility e.g Adult in Economy and Child in Business class + + public int? ReturnFareId { get; set; } + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Data/DTO/BookingOrderDTO.cs b/BookingService/FlightBooking.Service/Data/DTO/BookingOrderDTO.cs new file mode 100644 index 0000000..80a6ee1 --- /dev/null +++ b/BookingService/FlightBooking.Service/Data/DTO/BookingOrderDTO.cs @@ -0,0 +1,25 @@ +using System.ComponentModel.DataAnnotations; + +namespace FlightBooking.Service.Data.DTO +{ + public class BookingOrderDTO + { + [Required] + public string Email { get; set; } = null!; + + [Required] + public string OutboundFlightNumber { get; set; } = null!; + + public string? ReturnFlightNumber { get; set; } + + [Required] + public List<BookingRequestDTO> Bookings { get; set; } = new List<BookingRequestDTO>(); + } + + public class BookingResponseDTO + { + public string OrderNumber { get; set; } = null!; + public string PaymentLink { get; set; } = null!; + public DateTime OrderExpiration { get; set; } = DateTime.UtcNow.AddHours(1); + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Data/DTO/FlightFareDTO.cs b/BookingService/FlightBooking.Service/Data/DTO/FlightFareDTO.cs new file mode 100644 index 0000000..ae7db01 --- /dev/null +++ b/BookingService/FlightBooking.Service/Data/DTO/FlightFareDTO.cs @@ -0,0 +1,16 @@ +namespace FlightBooking.Service.Data.DTO +{ + public class FlightFareDTO : BookingFlightFareDTO + { + public int Id { get; set; } + public decimal AvailableSeats { get; set; } + } + + public class BookingFlightFareDTO + { + public string FareCode { get; set; } = null!; + public string FareName { get; set; } = null!; + public decimal Price { get; set; } + public string FlightNumber { get; set; } = null!; + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Data/DTO/FlightInformationDTO.cs b/BookingService/FlightBooking.Service/Data/DTO/FlightInformationDTO.cs new file mode 100644 index 0000000..eb8acda --- /dev/null +++ b/BookingService/FlightBooking.Service/Data/DTO/FlightInformationDTO.cs @@ -0,0 +1,19 @@ +namespace FlightBooking.Service.Data.DTO +{ + public class FlightInformationDTO : BookingFlightInformationDTO + { + public int Id { get; set; } + public int SeatCapacity { get; set; } + public int AvailableSeats { get; set; } + } + + public class BookingFlightInformationDTO + { + public string FlightNumber { get; set; } = null!; + public string Origin { get; set; } = null!; + public string Destination { get; set; } = null!; + public DateTime DepartureDate { get; set; } + public DateTime ArrivalDate { get; set; } + public string Airline { get; set; } = null!; + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Data/DTO/ReservedSeatDTO.cs b/BookingService/FlightBooking.Service/Data/DTO/ReservedSeatDTO.cs new file mode 100644 index 0000000..862b363 --- /dev/null +++ b/BookingService/FlightBooking.Service/Data/DTO/ReservedSeatDTO.cs @@ -0,0 +1,17 @@ +namespace FlightBooking.Service.Data.DTO +{ + public class ReservedSeatDTO + { + public string SeatNumber { get; set; } = null!; // e.g 1A, 33B + + public string FlightNumber { get; set; } = null!; + + public bool IsReserved { get; set; } + } + + public class ReservedSeatRequestDTO + { + public string SeatNumber { get; set; } = null!; // e.g 1A, 33B + public string BookingNumber { get; set; } = null!; + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Data/DTO/StripeDataDTO.cs b/BookingService/FlightBooking.Service/Data/DTO/StripeDataDTO.cs new file mode 100644 index 0000000..b6ca868 --- /dev/null +++ b/BookingService/FlightBooking.Service/Data/DTO/StripeDataDTO.cs @@ -0,0 +1,32 @@ +using System.ComponentModel.DataAnnotations; + +namespace FlightBooking.Service.Data.DTO +{ + public class StripeDataDTO + { + [Required] + public string SuccessUrl { get; set; } = null!; + + [Required] + public string CancelUrl { get; set; } = null!; + + [Required] + public string ProductName { get; set; } = null!; + + [Required] + public string ProductDescription { get; set; } = null!; + + [Required] + [Range(0.5, double.MaxValue)] //minimum of 50 cents + public decimal Amount { get; set; } + + [Required] + public string CustomerEmail { get; set; } = null!; + + [Required] + public string CurrencyCode { get; set; } = "USD"; + + [Required] + public string OrderNumber { get; set; } = null!; + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Data/DatabaseSeeding.cs b/BookingService/FlightBooking.Service/Data/DatabaseSeeding.cs new file mode 100644 index 0000000..bd27596 --- /dev/null +++ b/BookingService/FlightBooking.Service/Data/DatabaseSeeding.cs @@ -0,0 +1,158 @@ +using FlightBooking.Service.Data.Models; +using Microsoft.EntityFrameworkCore; + +namespace FlightBooking.Service.Data +{ + public static class DatabaseSeeding + { + public static void Initialize(IServiceProvider serviceProvider) + { + using (var context = new FlightBookingContext(serviceProvider.GetRequiredService<DbContextOptions<FlightBookingContext>>())) + { + if (context.FlightInformation.Any()) + { + return; + } + + string flightA = Guid.NewGuid().ToString("N")[..4].ToUpper(); + string flightB = Guid.NewGuid().ToString("N")[..4].ToUpper(); + List<FlightFare> flightFaresA = new List<FlightFare> + { + new FlightFare + { + FareName = "Economy class", + FareCode = "Eco-1", + Price = 4000, + SeatCapacity = 30, + SeatReserved = 0, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow, + }, + new FlightFare + { + FareName = "Business class", + FareCode = "Biz-1", + Price = 4000, + SeatCapacity = 30, + SeatReserved = 0, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow, + }, + }; + + List<FlightFare> flightFaresB = new List<FlightFare> + { + new FlightFare + { + FareName = "Economy class", + FareCode = "Eco-11", + Price = 5000, + SeatCapacity = 20, + SeatReserved = 0, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow, + }, + new FlightFare + { + FareName = "Business class", + FareCode = "Biz-11", + Price = 4000, + SeatCapacity = 10, + SeatReserved = 0, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow, + }, + }; + + //Create available seats + var reservedSeatsA = GenerateSeats(flightA, 60); + var reservedSeatsB = GenerateSeats(flightB, 60); + + List<FlightInformation> flights = new List<FlightInformation> + { + new FlightInformation + { + SeatCapacity = 60, + DepartureDate = DateTime.UtcNow.AddMonths(3), + ArrivalDate = DateTime.UtcNow.AddMonths(3).AddHours(4), + Airline = "Emirates", + SeatReserved = 0, + Destination = "London", + Origin = "Lagos", + FlightNumber = flightA, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow, + FlightFares = flightFaresA, + ReservedSeats = reservedSeatsA, + }, + new FlightInformation + { + SeatCapacity = 40, + DepartureDate = DateTime.UtcNow.AddMonths(3).AddDays(7), + ArrivalDate = DateTime.UtcNow.AddMonths(3).AddDays(7).AddHours(4), + Airline = "Emirates", + SeatReserved = 0, + Destination = "Lagos", + Origin = "London", + FlightNumber = flightB, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow, + FlightFares = flightFaresB, + ReservedSeats = reservedSeatsB + } + }; + + context.FlightInformation.AddRange(flights); + + context.SaveChanges(); + + context.Database.EnsureCreated(); + } + } + + private static List<ReservedSeat> GenerateSeats(string flightNumber, int flightCapacity) + { + //assume seats are in group of 4 Alphabets e.g 1A, 1B, 1C, 1D + + Dictionary<int, string> SeatMaps = new Dictionary<int, string> + { + {1, "A" }, + {2, "B" }, + {3, "C" }, + {4, "D" } + }; + + int seatId = 1; + int seatCount = 1; + + List<string> seatNumbers = new List<string>(); + + for (int i = 1; i < flightCapacity + 1; i++) + { + if (seatCount > 4) + { + seatId++; + seatCount = 1; + } + + seatNumbers.Add(seatId + SeatMaps[seatCount]); + seatCount++; + } + + List<ReservedSeat> reservedSeats = new List<ReservedSeat>(); + + foreach (var seatNumber in seatNumbers) + { + reservedSeats.Add(new ReservedSeat + { + BookingNumber = null, + FlightNumber = flightNumber, + IsReserved = false, + SeatNumber = seatNumber + }); + } + + return reservedSeats; + } + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Data/Enums.cs b/BookingService/FlightBooking.Service/Data/Enums.cs new file mode 100644 index 0000000..15a8a62 --- /dev/null +++ b/BookingService/FlightBooking.Service/Data/Enums.cs @@ -0,0 +1,40 @@ +namespace FlightBooking.Service.Data +{ + public enum Gender + { + Female = 1, + Male, + Transgender, + Fluid, + PreferNotToSay + } + + public enum InternalCode + { + UpdateError = -1, + Failed, + Success, + EntityIsNull, + EntityNotFound, + Mismatch, + InvalidParam, + Incompleted, + ListEmpty, + EntityExist, + Unprocessable, + Unauthorized, + } + + public enum SortOrder + { + ASC = 1, + DESC = 2 + } + + public enum BookingStatus + { + Pending = 1, + Confirmed, + Paid + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Data/FlightBookingContext.cs b/BookingService/FlightBooking.Service/Data/FlightBookingContext.cs new file mode 100644 index 0000000..ecdf63e --- /dev/null +++ b/BookingService/FlightBooking.Service/Data/FlightBookingContext.cs @@ -0,0 +1,70 @@ +using FlightBooking.Service.Data.ModelConfigurations; +using FlightBooking.Service.Data.Models; +using Microsoft.EntityFrameworkCore; + +namespace FlightBooking.Service.Data +{ + public class FlightBookingContext : DbContext + { + public FlightBookingContext(DbContextOptions<FlightBookingContext> options) : base(options) + { + } + + public DbSet<Booking> Bookings { get; set; } + public DbSet<BookingOrder> BookingOrders { get; set; } + public DbSet<ReservedSeat> ReservedSeats { get; set; } + public DbSet<Payment> Payments { get; set; } + public DbSet<FlightFare> FlightFares { get; set; } + public DbSet<FlightInformation> FlightInformation { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + //configure each model + + modelBuilder.ApplyConfiguration(new FlightInformationConfiguration()); + modelBuilder.ApplyConfiguration(new FlightFareConfiguration()); + modelBuilder.ApplyConfiguration(new BookingConfiguration()); + modelBuilder.ApplyConfiguration(new BookingOrderConfiguration()); + modelBuilder.ApplyConfiguration(new PaymentConfiguration()); + modelBuilder.ApplyConfiguration(new ReservedSeatConfiguration()); + + ////Ensure all dates are saved as UTC and read as UTC: + ////https://github.com/dotnet/efcore/issues/4711#issuecomment-481215673 + + foreach (var entityType in modelBuilder.Model.GetEntityTypes()) + { + foreach (var property in entityType.GetProperties()) + { + if (property.ClrType == typeof(DateTime)) + { + modelBuilder.Entity(entityType.ClrType) + .Property<DateTime>(property.Name) + .HasConversion( + v => v.ToUniversalTime(), + v => DateTime.SpecifyKind(v, DateTimeKind.Utc)); + } + else if (property.ClrType == typeof(DateTime?)) + { + modelBuilder.Entity(entityType.ClrType) + .Property<DateTime?>(property.Name) + .HasConversion( + v => v.HasValue ? v.Value.ToUniversalTime() : v, + v => v.HasValue ? DateTime.SpecifyKind(v.Value, DateTimeKind.Utc) : v); + } + } + } + } + + public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default) + { + return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); + } + + //protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + //{ + // base.OnConfiguring(optionsBuilder); + //} + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Data/ModelConfigurations/BookingConfiguration.cs b/BookingService/FlightBooking.Service/Data/ModelConfigurations/BookingConfiguration.cs new file mode 100644 index 0000000..a07fc63 --- /dev/null +++ b/BookingService/FlightBooking.Service/Data/ModelConfigurations/BookingConfiguration.cs @@ -0,0 +1,27 @@ +using FlightBooking.Service.Data.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace FlightBooking.Service.Data.ModelConfigurations +{ + public class BookingConfiguration : IEntityTypeConfiguration<Booking> + { + public void Configure(EntityTypeBuilder<Booking> entity) + { + entity.HasOne(d => d.BookingOrder).WithMany(p => p.Bookings) + .HasPrincipalKey(p => p.Id) + .HasForeignKey(d => d.BookingOrderId) + .OnDelete(DeleteBehavior.ClientSetNull); + + entity.HasOne(d => d.FlightInformation).WithMany(p => p.Bookings) + .HasPrincipalKey(p => p.Id) + .HasForeignKey(d => d.FlightId) + .OnDelete(DeleteBehavior.ClientSetNull); + + entity.HasOne(d => d.FlightFare).WithMany(p => p.Bookings) + .HasPrincipalKey(p => p.Id) + .HasForeignKey(d => d.FlightFareId) + .OnDelete(DeleteBehavior.ClientSetNull); + } + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Data/ModelConfigurations/BookingOrderConfiguration.cs b/BookingService/FlightBooking.Service/Data/ModelConfigurations/BookingOrderConfiguration.cs new file mode 100644 index 0000000..7ab6170 --- /dev/null +++ b/BookingService/FlightBooking.Service/Data/ModelConfigurations/BookingOrderConfiguration.cs @@ -0,0 +1,13 @@ +using FlightBooking.Service.Data.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace FlightBooking.Service.Data.ModelConfigurations +{ + public class BookingOrderConfiguration : IEntityTypeConfiguration<BookingOrder> + { + public void Configure(EntityTypeBuilder<BookingOrder> entity) + { + } + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Data/ModelConfigurations/FlightFareConfiguration.cs b/BookingService/FlightBooking.Service/Data/ModelConfigurations/FlightFareConfiguration.cs new file mode 100644 index 0000000..298a02f --- /dev/null +++ b/BookingService/FlightBooking.Service/Data/ModelConfigurations/FlightFareConfiguration.cs @@ -0,0 +1,17 @@ +using FlightBooking.Service.Data.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace FlightBooking.Service.Data.ModelConfigurations +{ + public class FlightFareConfiguration : IEntityTypeConfiguration<FlightFare> + { + public void Configure(EntityTypeBuilder<FlightFare> entity) + { + entity.HasOne(d => d.FlightInformation).WithMany(p => p.FlightFares) + .HasPrincipalKey(p => p.Id) + .HasForeignKey(d => d.FlightInformationId) + .OnDelete(DeleteBehavior.ClientSetNull); + } + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Data/ModelConfigurations/FlightInformationConfiguration.cs b/BookingService/FlightBooking.Service/Data/ModelConfigurations/FlightInformationConfiguration.cs new file mode 100644 index 0000000..2af41e5 --- /dev/null +++ b/BookingService/FlightBooking.Service/Data/ModelConfigurations/FlightInformationConfiguration.cs @@ -0,0 +1,13 @@ +using FlightBooking.Service.Data.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace FlightBooking.Service.Data.ModelConfigurations +{ + public class FlightInformationConfiguration : IEntityTypeConfiguration<FlightInformation> + { + public void Configure(EntityTypeBuilder<FlightInformation> entity) + { + } + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Data/ModelConfigurations/PaymentConfiguration.cs b/BookingService/FlightBooking.Service/Data/ModelConfigurations/PaymentConfiguration.cs new file mode 100644 index 0000000..5b13234 --- /dev/null +++ b/BookingService/FlightBooking.Service/Data/ModelConfigurations/PaymentConfiguration.cs @@ -0,0 +1,17 @@ +using FlightBooking.Service.Data.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace FlightBooking.Service.Data.ModelConfigurations +{ + public class PaymentConfiguration : IEntityTypeConfiguration<Payment> + { + public void Configure(EntityTypeBuilder<Payment> entity) + { + entity.HasOne(d => d.BookingOrder).WithMany(p => p.Payments) + .HasPrincipalKey(p => p.Id) + .HasForeignKey(d => d.BookingOrderId) + .OnDelete(DeleteBehavior.ClientSetNull); + } + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Data/ModelConfigurations/ReservedSeatConfiguration.cs b/BookingService/FlightBooking.Service/Data/ModelConfigurations/ReservedSeatConfiguration.cs new file mode 100644 index 0000000..9ca62f1 --- /dev/null +++ b/BookingService/FlightBooking.Service/Data/ModelConfigurations/ReservedSeatConfiguration.cs @@ -0,0 +1,17 @@ +using FlightBooking.Service.Data.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace FlightBooking.Service.Data.ModelConfigurations +{ + public class ReservedSeatConfiguration : IEntityTypeConfiguration<ReservedSeat> + { + public void Configure(EntityTypeBuilder<ReservedSeat> entity) + { + entity.HasOne(d => d.FlightInformation).WithMany(p => p.ReservedSeats) + .HasPrincipalKey(p => p.Id) + .HasForeignKey(d => d.FlightInformationId) + .OnDelete(DeleteBehavior.ClientSetNull); + } + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Data/Models/Booking.cs b/BookingService/FlightBooking.Service/Data/Models/Booking.cs new file mode 100644 index 0000000..315ea18 --- /dev/null +++ b/BookingService/FlightBooking.Service/Data/Models/Booking.cs @@ -0,0 +1,36 @@ +using System.ComponentModel.DataAnnotations; + +namespace FlightBooking.Service.Data.Models +{ + public class Booking + { + [Key] + public int Id { get; set; } + + public string FirstName { get; set; } = null!; + public string LastName { get; set; } = null!; + public string PhoneNumber { get; set; } = null!; + public string? Email { get; set; } + public string Address { get; set; } = null!; + public DateOnly? DateOfBirth { get; set; } + public Gender Gender { get; set; } + + public string BookingNumber { get; set; } = null!; + public int BookingOrderId { get; set; } + + public BookingStatus BookingStatus { get; set; } = BookingStatus.Pending; + + public int FlightId { get; set; } + public int FlightFareId { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + + public virtual FlightInformation FlightInformation { get; set; } = null!; + public virtual FlightFare FlightFare { get; set; } = null!; + public virtual BookingOrder BookingOrder { get; set; } = null!; + + //one to one to for seats. one booking at most one seat + + public ReservedSeat? ReservedSeat { get; set; } + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Data/Models/BookingOrder.cs b/BookingService/FlightBooking.Service/Data/Models/BookingOrder.cs new file mode 100644 index 0000000..5dce348 --- /dev/null +++ b/BookingService/FlightBooking.Service/Data/Models/BookingOrder.cs @@ -0,0 +1,26 @@ +using Microsoft.EntityFrameworkCore; +using System.ComponentModel.DataAnnotations; + +namespace FlightBooking.Service.Data.Models +{ + public class BookingOrder + { + [Key] + public int Id { get; set; } + + public string OrderNumber { get; set; } = null!; + public string Email { get; set; } = null!; + + [Precision(19, 4)] + public decimal TotalAmount { get; set; } + + public BookingStatus OrderStatus { get; set; } = BookingStatus.Pending; + public int NumberOfAdults { get; set; } + public int NumberOfChildren { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + + public ICollection<Booking> Bookings { get; set; } = new List<Booking>(); + public ICollection<Payment> Payments { get; set; } = new List<Payment>(); + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Data/Models/FlightFare.cs b/BookingService/FlightBooking.Service/Data/Models/FlightFare.cs new file mode 100644 index 0000000..9dfca35 --- /dev/null +++ b/BookingService/FlightBooking.Service/Data/Models/FlightFare.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore; +using System.ComponentModel.DataAnnotations; + +namespace FlightBooking.Service.Data.Models +{ + public class FlightFare + { + [Key] + public int Id { get; set; } + + public int FlightInformationId { get; set; } + public string FareCode { get; set; } = null!; + public string FareName { get; set; } = null!; + + [Precision(19, 4)] + public decimal Price { get; set; } + + public int SeatCapacity { get; set; } + public int SeatReserved { get; set; } + + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + + public ICollection<Booking> Bookings { get; set; } = new List<Booking>(); + + public virtual FlightInformation FlightInformation { get; set; } = null!; + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Data/Models/FlightInformation.cs b/BookingService/FlightBooking.Service/Data/Models/FlightInformation.cs new file mode 100644 index 0000000..ed01d2e --- /dev/null +++ b/BookingService/FlightBooking.Service/Data/Models/FlightInformation.cs @@ -0,0 +1,25 @@ +using System.ComponentModel.DataAnnotations; + +namespace FlightBooking.Service.Data.Models +{ + public class FlightInformation + { + [Key] + public int Id { get; set; } + + public string FlightNumber { get; set; } = null!; + public string Origin { get; set; } = null!; + public string Destination { get; set; } = null!; + public DateTime DepartureDate { get; set; } + public DateTime ArrivalDate { get; set; } + public string Airline { get; set; } = null!; + public int SeatCapacity { get; set; } + public int SeatReserved { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + + public ICollection<Booking> Bookings { get; set; } = new List<Booking>(); + public ICollection<FlightFare> FlightFares { get; set; } = new List<FlightFare>(); + public ICollection<ReservedSeat> ReservedSeats { get; set; } = new List<ReservedSeat>(); + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Data/Models/PassengerInformation.cs b/BookingService/FlightBooking.Service/Data/Models/PassengerInformation.cs new file mode 100644 index 0000000..1bcb851 --- /dev/null +++ b/BookingService/FlightBooking.Service/Data/Models/PassengerInformation.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; + +namespace FlightBooking.Service.Data.Models +{ + public class PassengerInformation + { + [Key] + public int Id { get; set; } + + public string FirstName { get; set; } = null!; + public string LastName { get; set; } = null!; + public string? PhoneNumber { get; set; } + public string? Email { get; set; } + public string Address { get; set; } = null!; + public DateOnly DateOfBirth { get; set; } + public Gender Gender { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Data/Models/Payment.cs b/BookingService/FlightBooking.Service/Data/Models/Payment.cs new file mode 100644 index 0000000..4e9e2cb --- /dev/null +++ b/BookingService/FlightBooking.Service/Data/Models/Payment.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore; +using System.ComponentModel.DataAnnotations; + +namespace FlightBooking.Service.Data.Models +{ + public class Payment + { + [Key] + public int Id { get; set; } + + public string CustomerEmail { get; set; } = null!; + + [Precision(19, 4)] + public decimal TransactionAmount { get; set; } + + public string PaymentReference { get; set; } = null!; + public string OrderNumber { get; set; } = null!; + public string CurrencyCode { get; set; } = null!; + public string PaymentChannel { get; set; } = null!; + public string PaymentStatus { get; set; } = null!; + public int BookingOrderId { get; set; } + public DateTime TransactionDate { get; set; } + public string? MetaData { get; set; } + + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + + public virtual BookingOrder BookingOrder { get; set; } = null!; + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Data/Models/ReservedSeat.cs b/BookingService/FlightBooking.Service/Data/Models/ReservedSeat.cs new file mode 100644 index 0000000..6f6fb69 --- /dev/null +++ b/BookingService/FlightBooking.Service/Data/Models/ReservedSeat.cs @@ -0,0 +1,22 @@ +using System.ComponentModel.DataAnnotations; + +namespace FlightBooking.Service.Data.Models +{ + public class ReservedSeat + { + [Key] + public int Id { get; set; } + + public string SeatNumber { get; set; } = null!; // e.g 1A, 33B + public string? BookingNumber { get; set; } + public int? BookingId { get; set; } //FK to Booking + public string FlightNumber { get; set; } = null!; + public int FlightInformationId { get; set; } + public bool IsReserved { get; set; } = false; + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + + public virtual FlightInformation FlightInformation { get; set; } = null!; + public virtual Booking? Booking { get; set; } + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Data/Repository/GenericRepository.cs b/BookingService/FlightBooking.Service/Data/Repository/GenericRepository.cs new file mode 100644 index 0000000..584354a --- /dev/null +++ b/BookingService/FlightBooking.Service/Data/Repository/GenericRepository.cs @@ -0,0 +1,257 @@ +using Microsoft.EntityFrameworkCore; +using System.Linq.Expressions; + +namespace FlightBooking.Service.Data.Repository +{ + public class GenericRepository<T> : IGenericRepository<T> where T : class, new() + { + //Responses: failed=0, success=1 + + //IEnumerable iterates over an in-memory collection while IQueryable does so on the DB + // call to .ToList to enable instant query against DB + + protected FlightBookingContext _db; + protected ILogger _logger; + + public GenericRepository(FlightBookingContext db, ILogger<GenericRepository<T>> logger) + { + _logger = logger; + _db = db; + } + + public IQueryable<T> GetAll() + { + return _db.Set<T>(); + } + + public DbSet<T> GetDbSet() + { + return _db.Set<T>(); + } + + public IQueryable<T> Query() + { + return _db.Set<T>().AsQueryable(); + } + + #region Async Methods + + public async Task<T?> GetByIdAsync(int id) + { + var entity = await _db.Set<T>().FindAsync(id); + + return entity; + } + + public async Task<T?> GetByGuidAsync(Guid id) + { + var entity = await _db.Set<T>().FindAsync(id); + + return entity; + } + + public async Task<int> CreateAsync(T entity, bool isSave = true) + { + if (entity == null) + { + _logger.LogError(RepositoryConstants.CreateNullError, typeof(T).Name); + return (int)InternalCode.EntityIsNull; + } + + _db.Set<T>().Add(entity); + + if (isSave) + { + return await SaveChangesToDbAsync(); + } + + return (int)InternalCode.Success; + } + + public async Task<int> UpdateAsync(T entity, bool isSave = true) + { + //Check for this in each overriding implementation or services + //var prev = await GetById(id); + + //if (prev == null) + //{ + // return 0; + //} + + _db.Set<T>().Update(entity); + + if (isSave) + { + return await SaveChangesToDbAsync(); + } + + return (int)InternalCode.Success; + } + + public async Task<int> DeleteAsync(int id, bool isSave = true) + { + T? entity = await GetByIdAsync(id); + + if (entity == null) + { + _logger.LogError(RepositoryConstants.DeleteNullError, typeof(T).Name); + return (int)InternalCode.EntityNotFound; + } + + _db.Set<T>().Remove(entity); + + if (isSave) + { + return await SaveChangesToDbAsync(); + } + + return (int)InternalCode.Success; + } + + public async Task<int> BulkDeleteAsync(IEnumerable<int> entityId, bool isSave = true) + { + if (entityId == null || !entityId.Any()) + { + _logger.LogError(RepositoryConstants.BulkDeleteNullError, typeof(T).Name); + return (int)InternalCode.EntityIsNull; + } + + DbSet<T> table = _db.Set<T>(); + + foreach (int id in entityId) + { + T? entity = await GetByIdAsync(id); + if (entity != null) + { + table.Remove(entity); + } + } + + if (isSave) + { + return await SaveChangesToDbAsync(); + } + + return (int)InternalCode.Success; + } + + public async Task<int> BulkCreateAsync(IEnumerable<T> entities, bool isSave = true) + { + if (entities == null || !entities.Any()) + { + _logger.LogError(RepositoryConstants.BulkCreateNullError, typeof(T).Name); + return (int)InternalCode.EntityIsNull; + } + + DbSet<T> table = _db.Set<T>(); + + table.AddRange(entities); + + if (isSave) + { + return await SaveChangesToDbAsync(); + } + + return (int)InternalCode.Success; + } + + //calling this once works since we are using just one DbContext + //TODO: returning 0 should not lead to 500 error. 0 means no entries were added which may be because all entries have been added already + //fix this after tests have been writing for projects + public async Task<int> SaveChangesToDbAsync() + { + _logger.LogInformation(RepositoryConstants.LoggingStarted); + int saveResult; + + try + { + int tempResult = await _db.SaveChangesAsync(); //give numbers of entries updated in db. in some cases e.g Update, when no data changes, this method returns 0 + if (tempResult == 0) + { + _logger.LogInformation(RepositoryConstants.EmptySaveInfo); + } + saveResult = (int)InternalCode.Success; //means atleast one entry was made. 1 is InternalCode.Success. + //saveResult = tempResult > 0 ? 1 : 0; //means atleast one entry was made. 1 is InternalCode.Success + } + catch (DbUpdateConcurrencyException ex) + { + _logger.LogError(ex, RepositoryConstants.UpdateConcurrencyException); + saveResult = (int)InternalCode.UpdateError; + throw; + } + catch (DbUpdateException ex) + { + _logger.LogError(ex, RepositoryConstants.UpdateException); + saveResult = (int)InternalCode.UpdateError; + throw; + } + catch (Exception ex) + { + _logger.LogError(ex, RepositoryConstants.SaveChangesException); + saveResult = (int)InternalCode.UpdateError; + throw; + } + return saveResult; + } + + public async Task<bool> EntityExistsAsync(int id) + { + T? entityFound = await _db.Set<T>().FindAsync(id); + if (entityFound == null) + { + return false; + } + + return true; + } + + #endregion Async Methods + + public IQueryable<T> OrderByText(IQueryable<T> data, SortOrder order, Expression<Func<T, string>> expression) + { + IQueryable<T> orderedData; + if (order == SortOrder.ASC) + { + orderedData = data.OrderBy(expression); + } + else + { + orderedData = data.OrderByDescending(expression); + } + + return orderedData; + } + + public IQueryable<T> OrderByDate(IQueryable<T> data, SortOrder order, Expression<Func<T, DateTime>> expression) + { + IQueryable<T> orderedData; + if (order == SortOrder.ASC) + { + orderedData = data.OrderBy(expression); + } + else + { + orderedData = data.OrderByDescending(expression); + } + + return orderedData; + } + + public async Task<List<T>> TakeAndSkipAsync(IQueryable<T> data, int pageSize, int pageIndex) + { + //List<T> paginatedList = new List<T>(); + + //if (data == null || data.Count() <= 0) + // return paginatedList; + + //if (pageSize == 0 && pageIndex == 0) + // return paginatedList; + + int numRowSkipped = pageSize * (pageIndex - 1); + + List<T> paginated = await data.Skip(numRowSkipped).Take(pageSize).ToListAsync(); + + return paginated; + } + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Data/Repository/IGenericRepository.cs b/BookingService/FlightBooking.Service/Data/Repository/IGenericRepository.cs new file mode 100644 index 0000000..74e90c8 --- /dev/null +++ b/BookingService/FlightBooking.Service/Data/Repository/IGenericRepository.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore; +using System.Linq.Expressions; + +namespace FlightBooking.Service.Data.Repository +{ + public interface IGenericRepository<T> where T : class + { + Task<int> BulkCreateAsync(IEnumerable<T> entities, bool isSave = true); + + Task<int> BulkDeleteAsync(IEnumerable<int> ids, bool isSave = true); + + Task<int> CreateAsync(T entity, bool isSave = true); + + Task<int> DeleteAsync(int id, bool isSave = true); + + Task<bool> EntityExistsAsync(int id); + + Task<T?> GetByIdAsync(int id); + + Task<T?> GetByGuidAsync(Guid id); + + Task<int> UpdateAsync(T entity, bool isSave = true); + + Task<int> SaveChangesToDbAsync(); + + DbSet<T> GetDbSet(); + + IQueryable<T> Query(); + + Task<List<T>> TakeAndSkipAsync(IQueryable<T> data, int pageSize, int pageIndex); + + IQueryable<T> OrderByText(IQueryable<T> data, SortOrder order, Expression<Func<T, string>> expression); + + IQueryable<T> OrderByDate(IQueryable<T> data, SortOrder order, Expression<Func<T, DateTime>> expression); + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Data/Repository/RepositoryModule.cs b/BookingService/FlightBooking.Service/Data/Repository/RepositoryModule.cs new file mode 100644 index 0000000..90b6c0e --- /dev/null +++ b/BookingService/FlightBooking.Service/Data/Repository/RepositoryModule.cs @@ -0,0 +1,17 @@ +using FlightBooking.Service.Data.Models; + +namespace FlightBooking.Service.Data.Repository +{ + public static class RepositoryModule + { + public static void AddRepository(this IServiceCollection services) + { + services.AddScoped<IGenericRepository<Booking>, GenericRepository<Booking>>(); + services.AddScoped<IGenericRepository<FlightInformation>, GenericRepository<FlightInformation>>(); + services.AddScoped<IGenericRepository<FlightFare>, GenericRepository<FlightFare>>(); + services.AddScoped<IGenericRepository<Payment>, GenericRepository<Payment>>(); + services.AddScoped<IGenericRepository<BookingOrder>, GenericRepository<BookingOrder>>(); + services.AddScoped<IGenericRepository<ReservedSeat>, GenericRepository<ReservedSeat>>(); + } + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/FlightBooking.Service.csproj b/BookingService/FlightBooking.Service/FlightBooking.Service.csproj new file mode 100644 index 0000000..217d79c --- /dev/null +++ b/BookingService/FlightBooking.Service/FlightBooking.Service.csproj @@ -0,0 +1,45 @@ +<Project Sdk="Microsoft.NET.Sdk.Web"> + + <PropertyGroup> + <TargetFramework>net8.0</TargetFramework> + <Nullable>enable</Nullable> + <ImplicitUsings>enable</ImplicitUsings> + </PropertyGroup> + <PropertyGroup> + <EnableNETAnalyzers>true</EnableNETAnalyzers> + <UserSecretsId>b1b4ccfd-9fcb-4702-b4e5-c4e88d0bc805</UserSecretsId> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="AutoMapper" Version="13.0.1" /> + <PackageReference Include="Azure.Identity" Version="1.10.4" /> + <PackageReference Include="libphonenumber-csharp" Version="8.13.30" /> + <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.2" /> + <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.2" /> + <PackageReference Include="Microsoft.Data.SqlClient" Version="5.1.5" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.2"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.2"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="8.0.1" /> + <PackageReference Include="MySqlConnector" Version="2.3.5" /> + <PackageReference Include="NLog.Web.AspNetCore" Version="5.3.8" /> + <PackageReference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="7.3.1" /> + + <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.2" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.2" /> + <PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="8.0.0" /> + <PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="7.3.1" /> + <PackageReference Include="NuGet.Common" Version="6.9.1" /> + <PackageReference Include="NuGet.Protocol" Version="6.9.1" /> + <PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.1" /> + <PackageReference Include="Stripe.net" Version="43.18.0" /> + <PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" /> + <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.3.1" /> + </ItemGroup> + +</Project> diff --git a/BookingService/FlightBooking.Service/FlightBooking.Service.http b/BookingService/FlightBooking.Service/FlightBooking.Service.http new file mode 100644 index 0000000..045f70b --- /dev/null +++ b/BookingService/FlightBooking.Service/FlightBooking.Service.http @@ -0,0 +1,6 @@ +@FlightBooking.Service_HostAddress = http://localhost:5000 + +GET {{FlightBooking.Service_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/BookingService/FlightBooking.Service/Middleware/ErrorHandlingMiddleware.cs b/BookingService/FlightBooking.Service/Middleware/ErrorHandlingMiddleware.cs new file mode 100644 index 0000000..1510d26 --- /dev/null +++ b/BookingService/FlightBooking.Service/Middleware/ErrorHandlingMiddleware.cs @@ -0,0 +1,6 @@ +namespace FlightBooking.Service.Middleware +{ + public class ErrorHandlingMiddleware + { + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Middleware/MiddlewareExtensions.cs b/BookingService/FlightBooking.Service/Middleware/MiddlewareExtensions.cs new file mode 100644 index 0000000..6b50b93 --- /dev/null +++ b/BookingService/FlightBooking.Service/Middleware/MiddlewareExtensions.cs @@ -0,0 +1,10 @@ +namespace FlightBooking.Service.Middleware +{ + public static class MiddlewareExtensions + { + public static IApplicationBuilder UseErrorHandlingMiddleware(this IApplicationBuilder builder) + { + return builder.UseMiddleware<ErrorHandlingMiddleware>(); + } + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Program.cs b/BookingService/FlightBooking.Service/Program.cs new file mode 100644 index 0000000..041f1b1 --- /dev/null +++ b/BookingService/FlightBooking.Service/Program.cs @@ -0,0 +1,63 @@ +using FlightBooking.Service.Data; +using NLog; +using NLog.Web; + +namespace FlightBooking.Service +{ + public class Program + { + public static void Main(string[] args) + { + var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger(); + + try + { + logger.Debug("init main"); + + var host = CreateHostBuilder(args).Build(); + + using (var scope = host.Services.CreateScope()) + { + var services = scope.ServiceProvider; + + try + { + DatabaseSeeding.Initialize(services); + } + catch (Exception ex) + { + logger.Error(ex, "An error occurred seeding the DB."); + } + } + + host.Run(); + } + catch (Exception exception) + { + //NLog: catch setup errors + logger.Error(exception, "Stopped program because of exception"); + throw; + } + finally + { + // Ensure to flush and stop internal timers/threads before application-exit (Avoid segmentation fault on Linux) + LogManager.Shutdown(); + } + } + + public static IHostBuilder CreateHostBuilder(string[] args) + { + return Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup<Startup>(); + }) + .ConfigureLogging(logging => + { + logging.ClearProviders(); + logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Information); + }) + .UseNLog(); // NLog: Setup NLog for Dependency injection + } + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Properties/launchSettings.json b/BookingService/FlightBooking.Service/Properties/launchSettings.json new file mode 100644 index 0000000..434256c --- /dev/null +++ b/BookingService/FlightBooking.Service/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:37680", + "sslPort": 44321 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7287;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Services/BookingOrderService.cs b/BookingService/FlightBooking.Service/Services/BookingOrderService.cs new file mode 100644 index 0000000..f1d70bd --- /dev/null +++ b/BookingService/FlightBooking.Service/Services/BookingOrderService.cs @@ -0,0 +1,391 @@ +using FlightBooking.Service.Data; +using FlightBooking.Service.Data.DTO; +using FlightBooking.Service.Data.Models; +using FlightBooking.Service.Data.Repository; +using FlightBooking.Service.Services.Interfaces; +using Microsoft.EntityFrameworkCore; + +namespace FlightBooking.Service.Services +{ + ///<inheritdoc /> + public class BookingOrderService : IBookingOrderService + { + private readonly IGenericRepository<BookingOrder> _orderRepo; + private readonly IGenericRepository<FlightInformation> _flightRepo; + private readonly IGenericRepository<FlightFare> _flightFareRepo; + private readonly IStripeService _stripeService; + + private readonly ILogger<BookingOrderService> _logger; + + private List<FlightFare> allFlightFares = new List<FlightFare>(); //declare once so we can use it throughout + + public BookingOrderService(IGenericRepository<BookingOrder> orderRepo, IGenericRepository<FlightInformation> flightRepo, + IGenericRepository<FlightFare> flightFareRepo, IStripeService stripeService, ILogger<BookingOrderService> logger) + { + _orderRepo = orderRepo; + _flightRepo = flightRepo; + _flightFareRepo = flightFareRepo; + _stripeService = stripeService; + _logger = logger; + } + + public async Task<ServiceResponse<BookingResponseDTO?>> CreateBookingOrderAsync(BookingOrderDTO order) + { + if (order == null) + { + return new ServiceResponse<BookingResponseDTO?>(null, InternalCode.InvalidParam); + } + + /* + Checks: + - outbound and return flights cannot be same + - Check if all the flights are available + - Check if the fares have available seats + - If Checks are not passed, return 422 + */ + + bool hasReturnFlight = !string.IsNullOrWhiteSpace(order.ReturnFlightNumber); + + //Outbound and Return flights cannot be the same + if (hasReturnFlight && order.ReturnFlightNumber == order.OutboundFlightNumber) + { + return new ServiceResponse<BookingResponseDTO?>(null, InternalCode.Unprocessable, "Outbound and return flights cannot be the same"); + } + + int totalFlights = order.Bookings.Count; + + FlightInformation outboundFlightInfo = new FlightInformation(); + FlightInformation returnFlightInfo = new FlightInformation(); + + //Check if the outbound flight is valid and if seats are available + var outboundFlight = CheckAvailableFlight(order.OutboundFlightNumber, totalFlights); + + //If flight not valid, return false + if (outboundFlight.FlightInformation == null) + { + return new ServiceResponse<BookingResponseDTO?>(null, InternalCode.Unprocessable, "The selected flight is not valid"); + } + + //if it's booked to Max, return error + if (outboundFlight.IsBookedToMax) + { + return new ServiceResponse<BookingResponseDTO?>(null, InternalCode.Unprocessable, "The number of flights exceed number of available seats"); + } + + outboundFlightInfo = outboundFlight.FlightInformation; + + //Check if Return flight available and not booked to max + + if (hasReturnFlight) + { + //If none of the Booking + + var returnFlight = CheckAvailableFlight(order.ReturnFlightNumber!, totalFlights); + + if (returnFlight.FlightInformation == null) + { + return new ServiceResponse<BookingResponseDTO?>(null, InternalCode.Unprocessable, "The selected flight is not valid"); + } + + if (returnFlight.IsBookedToMax) + { + return new ServiceResponse<BookingResponseDTO?>(null, InternalCode.Unprocessable, "The number of flights exceed number of available seats"); + } + + returnFlightInfo = returnFlight.FlightInformation; + } + + //Check if the selected Fares for each flight is valid and seats are available + var (IsAllFareExists, IsAnyFareMaxedOut, FareCodes) = CheckIfAnyFareIsMaxedOut(order, hasReturnFlight); + + if (!IsAllFareExists) + { + return new ServiceResponse<BookingResponseDTO?>(null, InternalCode.Unprocessable, "Some selected fare do not exist. Please check that all fare exists"); + } + + if (IsAnyFareMaxedOut) + { + return new ServiceResponse<BookingResponseDTO?>(null, InternalCode.Unprocessable, "The number of flights exceed number of available seats"); + } + + //if all checks passed, lets reduce number of available seats. + //If Checks are passed, set status as InProgress and reduce available seats to avoid overbooking the flight + //We hold a seat for N mins max(N is set in config). Use a background job to return UnbookedSeats back into the Pool + + await UpdateAvailableSeats(order, FareCodes); + + string orderReference = Guid.NewGuid().ToString("N")[..10].ToUpper(); + + //Create all the bookings + List<Booking> bookings = new List<Booking>(); + + decimal totalAmount = 0; + foreach (var booking in order.Bookings) + { + //TODO: Use AutoMapper + bookings.Add(new Booking + { + FirstName = booking.FirstName, + LastName = booking.LastName, + PhoneNumber = booking.PhoneNumber, + Email = booking.Email, + DateOfBirth = booking.DateOfBirth, + Address = booking.Address, + Gender = booking.Gender, + BookingNumber = Guid.NewGuid().ToString("N")[..10].ToUpper(), + BookingStatus = BookingStatus.Confirmed, + FlightId = outboundFlightInfo.Id, + FlightFareId = booking.OutboundFareId, + CreatedAt = DateTime.UtcNow + }); + + totalAmount += allFlightFares.FirstOrDefault(x => x.Id == booking.OutboundFareId)!.Price; + + //If return flight, add a seperate booking + if (hasReturnFlight) + { + bookings.Add(new Booking + { + FirstName = booking.FirstName, + LastName = booking.LastName, + PhoneNumber = booking.PhoneNumber, + Email = booking.Email, + DateOfBirth = booking.DateOfBirth, + Address = booking.Address, + Gender = booking.Gender, + BookingNumber = Guid.NewGuid().ToString("N")[..10].ToUpper(), + BookingStatus = BookingStatus.Confirmed, + FlightId = returnFlightInfo.Id, + FlightFareId = (int)booking.ReturnFareId!, + CreatedAt = DateTime.UtcNow + }); + + totalAmount += allFlightFares.FirstOrDefault(x => x.Id == booking.ReturnFareId)!.Price; + } + } + + //Sum all cost as part of Order + + BookingOrder bookingOrder = new BookingOrder + { + Bookings = bookings, + Email = order.Email, + OrderStatus = BookingStatus.Confirmed, + TotalAmount = totalAmount, + OrderNumber = orderReference, + CreatedAt = DateTime.UtcNow, + NumberOfAdults = 1, + NumberOfChildren = 1, + }; + + int result = await _orderRepo.CreateAsync(bookingOrder); + + if (result != 1) + { + return new ServiceResponse<BookingResponseDTO?>(null, (InternalCode)result); + } + + //Generate payment link with the cost if payment successful + + /* + Return Order Number, Payment Link, Payment Expiry + */ + StripeDataDTO stripeData = new StripeDataDTO + { + SuccessUrl = "https://localhost:44321/success", + CancelUrl = "https://localhost:44321/cancel", + ProductDescription = $"Booking for {orderReference}", + Amount = totalAmount, + CurrencyCode = "USD", + CustomerEmail = order.Email, + ProductName = "Flight Booking Service", + OrderNumber = orderReference, + }; + + var stripeResponse = _stripeService.GetStripeCheckoutUrl(stripeData); + + BookingResponseDTO bookingResponse = new BookingResponseDTO + { + OrderNumber = orderReference, + PaymentLink = stripeResponse.Data + }; + + return new ServiceResponse<BookingResponseDTO?>(bookingResponse, InternalCode.Success); + } + + public async Task<ServiceResponse<BookingResponseDTO?>> GetCheckoutUrlAsync(string orderNumber) + { + if (string.IsNullOrWhiteSpace(orderNumber)) + { + return new ServiceResponse<BookingResponseDTO?>(null, InternalCode.InvalidParam, "Order reference not supplied"); + } + + //gett the order details + var orderDetails = await _orderRepo.Query() + .FirstOrDefaultAsync(x => x.OrderNumber == orderNumber); + + if (orderDetails == null) + { + return new ServiceResponse<BookingResponseDTO?>(null, InternalCode.EntityNotFound, "The order with the supplied order number was not found"); + } + + if (orderDetails.OrderStatus == BookingStatus.Paid) + { + return new ServiceResponse<BookingResponseDTO?>(null, InternalCode.Unprocessable, "This order has already been paid for"); + } + + StripeDataDTO stripeData = new StripeDataDTO + { + SuccessUrl = "https://localhost:44321/success", + CancelUrl = "https://localhost:44321/cancel", + ProductDescription = $"Booking for Order : {orderDetails.OrderNumber}", + Amount = orderDetails.TotalAmount, + CurrencyCode = "USD", + CustomerEmail = orderDetails.Email, + ProductName = "Flight Booking Service", + OrderNumber = orderNumber + }; + + var stripeResponse = _stripeService.GetStripeCheckoutUrl(stripeData); + + BookingResponseDTO bookingResponse = new BookingResponseDTO + { + OrderNumber = orderNumber, + PaymentLink = stripeResponse.Data + }; + + return new ServiceResponse<BookingResponseDTO?>(bookingResponse, InternalCode.Success); + } + + private (FlightInformation? FlightInformation, bool IsBookedToMax) CheckAvailableFlight(string flightNumber, int totalFlights) + { + FlightInformation? flightInformation = _flightRepo.Query() + .FirstOrDefault(x => x.FlightNumber == flightNumber); + + //If flight not valid, return false + if (flightInformation == null) + { + return (flightInformation, false); + } + + int availableFlightCapacity = flightInformation.SeatCapacity - flightInformation.SeatReserved; + + return (flightInformation, totalFlights > availableFlightCapacity); + } + + private (bool IsAllFareExists, bool IsAnyFareMaxedOut, List<int> FareCodes) CheckIfAnyFareIsMaxedOut(BookingOrderDTO order, bool hasReturnFlight) + { + //get all flight fares for all the list ID. We can then use throughout the booking process + var validFlightFares = _flightFareRepo.Query() + .Include(x => x.FlightInformation) + .Where(x => x.FlightInformation.FlightNumber == order.OutboundFlightNumber); + + //if return flight, then add the fares + if (hasReturnFlight) + { + validFlightFares = _flightFareRepo.Query() + .Include(x => x.FlightInformation) + .Where(x => x.FlightInformation.FlightNumber == order.OutboundFlightNumber + || x.FlightInformation.FlightNumber == order.ReturnFlightNumber); + } + + //we get all the flight fares and save to the variable so we can reuse + allFlightFares = validFlightFares.ToList(); + + List<int> fareIds = new List<int>(); + + //get all fare Id + var outboundFares = order.Bookings + .Select(x => x.OutboundFareId) + .ToList(); + + //Add to a list + fareIds.AddRange(outboundFares); + + //we check if all fares are valid for that flight + var isAllFareValid = allFlightFares.Any(x => outboundFares.Contains(x.Id) + && x.FlightInformation.FlightNumber == order.OutboundFlightNumber); + + //if atleast one of the fare in the outbound flight is invalid, return false + if (!isAllFareValid) + { + return (false, true, new List<int>()); + } + + //check the codes for the return flights are also valid + if (hasReturnFlight) + { + var returnFares = order.Bookings + .Select(x => (int)x.ReturnFareId!) + .ToList(); + + fareIds.AddRange(returnFares); + + isAllFareValid = allFlightFares.Any(x => returnFares.Contains(x.Id) + && x.FlightInformation.FlightNumber == order.ReturnFlightNumber); + + if (!isAllFareValid) + { + return (false, true, new List<int>()); + } + } + + var flightFares = allFlightFares + .Select(y => new + { + y.Id, + y.FareCode, + AvailableSeats = y.SeatCapacity - y.SeatReserved + }).ToList(); + + //group the fares by Id + var fareGroup = fareIds.GroupBy(x => x).ToList(); + + bool isAnyFareMaxedOut = true; + + //for each fare, check if the seats are available + foreach (var group in fareGroup) + { + isAnyFareMaxedOut = flightFares.Any(x => x.Id == group.Key && group.Count() > x.AvailableSeats); + + //if any fare is maxed out, terminate the loop + if (isAnyFareMaxedOut) + { + break; + } + } + + return (true, isAnyFareMaxedOut, fareIds); + } + + private async Task UpdateAvailableSeats(BookingOrderDTO order, List<int> fareCodes) + { + int totalFlights = order.Bookings.Count; + + int result = await _flightRepo.Query() + .Where(x => x.FlightNumber == order.OutboundFlightNumber) + .ExecuteUpdateAsync(x => x.SetProperty(y => y.SeatReserved, y => y.SeatReserved + totalFlights)); + + //If a return is booked, reduce the seats too + if (!string.IsNullOrWhiteSpace(order.ReturnFlightNumber)) + { + result = await _flightRepo.Query() + .Where(x => x.FlightNumber == order.ReturnFlightNumber) + .ExecuteUpdateAsync(x => x.SetProperty(y => y.SeatReserved, y => y.SeatReserved + totalFlights)); + } + + //update fare capacity. includes all fares, initial and return + var grouped = fareCodes + .GroupBy(x => x).ToList(); + + foreach (var fare in grouped) + { + result = await _flightFareRepo.Query() + .Where(x => x.Id == fare.Key) + .ExecuteUpdateAsync(x => x.SetProperty(y => y.SeatReserved, y => y.SeatReserved + fare.Count())); + } + + return; + } + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Services/BookingService.cs b/BookingService/FlightBooking.Service/Services/BookingService.cs new file mode 100644 index 0000000..9dd41fe --- /dev/null +++ b/BookingService/FlightBooking.Service/Services/BookingService.cs @@ -0,0 +1,101 @@ +using AutoMapper; +using AutoMapper.QueryableExtensions; +using FlightBooking.Service.Data; +using FlightBooking.Service.Data.DTO; +using FlightBooking.Service.Data.Models; +using FlightBooking.Service.Data.Repository; +using FlightBooking.Service.Services.Interfaces; +using Microsoft.EntityFrameworkCore; + +namespace FlightBooking.Service.Services +{ + ///<inheritdoc /> + public class BookingService : IBookingService + { + private readonly IGenericRepository<Booking> _bookingRepo; + private readonly IMapper _mapper; + + public BookingService(IGenericRepository<Booking> bookingRepo, IMapper mapper) + { + _bookingRepo = bookingRepo; + _mapper = mapper; + } + + public async Task<ServiceResponse<BookingDTO?>> GetBookingByBookingNumberAsync(string bookingNumber) + { + if (string.IsNullOrWhiteSpace(bookingNumber)) + { + return new ServiceResponse<BookingDTO?>(null, InternalCode.InvalidParam, "No booking number supplied"); + } + + var booking = await _bookingRepo.Query() + .Include(x => x.FlightFare) + .Include(x => x.FlightInformation) + .Include(x => x.ReservedSeat) + .FirstOrDefaultAsync(x => x.BookingNumber == bookingNumber); + + if (booking == null) + { + return new ServiceResponse<BookingDTO?>(null, InternalCode.EntityNotFound, "No booking with that booking number exists"); + } + + var bookingDTO = _mapper.Map<Booking, BookingDTO>(booking); + + return new ServiceResponse<BookingDTO?>(bookingDTO, InternalCode.Success); + } + + public async Task<ServiceResponse<BookingDTO?>> GetBookingByBookingId(int bookingId) + { + var booking = await _bookingRepo.Query() + .Include(x => x.FlightFare) + .Include(x => x.FlightInformation) + .Include(x => x.ReservedSeat) + .FirstOrDefaultAsync(x => x.Id == bookingId); + + if (booking == null) + { + return new ServiceResponse<BookingDTO?>(null, InternalCode.EntityNotFound, "No booking with that booking ID exists"); + } + + var bookingDTO = _mapper.Map<Booking, BookingDTO>(booking); + + return new ServiceResponse<BookingDTO?>(bookingDTO, InternalCode.Success); + } + + public ServiceResponse<IEnumerable<BookingDTO>?> GetBookingsByEmail(string email) + { + if (string.IsNullOrWhiteSpace(email)) + { + return new ServiceResponse<IEnumerable<BookingDTO>?>(null, InternalCode.InvalidParam, "No email supplied"); + } + + var bookings = _bookingRepo.Query() + .Include(x => x.FlightFare) + .Include(x => x.FlightInformation) + .Include(x => x.ReservedSeat) + .Where(x => x.Email == email) + .ProjectTo<BookingDTO>(_mapper.ConfigurationProvider) + .ToList(); + + return new ServiceResponse<IEnumerable<BookingDTO>?>(bookings, InternalCode.Success); + } + + public ServiceResponse<IEnumerable<BookingDTO>?> GetBookingsByOrderNumber(string orderNumber) + { + if (string.IsNullOrWhiteSpace(orderNumber)) + { + return new ServiceResponse<IEnumerable<BookingDTO>?>(null, InternalCode.InvalidParam, "No order number supplied"); + } + + var bookings = _bookingRepo.Query() + .Include(x => x.FlightFare) + .Include(x => x.FlightInformation) + .Include(x => x.ReservedSeat) + .Where(x => x.BookingOrder.OrderNumber == orderNumber) + .ProjectTo<BookingDTO>(_mapper.ConfigurationProvider) + .ToList(); + + return new ServiceResponse<IEnumerable<BookingDTO>?>(bookings, InternalCode.Success); + } + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Services/FlightBookingProfile.cs b/BookingService/FlightBooking.Service/Services/FlightBookingProfile.cs new file mode 100644 index 0000000..4a7d890 --- /dev/null +++ b/BookingService/FlightBooking.Service/Services/FlightBookingProfile.cs @@ -0,0 +1,29 @@ +using AutoMapper; +using FlightBooking.Service.Data.DTO; +using FlightBooking.Service.Data.Models; + +namespace FlightBooking.Service.Services +{ + public class FlightBookingProfile : Profile + { + public FlightBookingProfile() + { + CreateMap<FlightFare, FlightFareDTO>() + .ForMember(dest => dest.AvailableSeats, opt => opt.MapFrom(src => src.SeatCapacity - src.SeatReserved)) + .ForMember(dest => dest.FlightNumber, opt => opt.MapFrom(src => src.FlightInformation.FlightNumber)); + + CreateMap<ReservedSeat, ReservedSeatDTO>(); + + CreateMap<FlightInformation, FlightInformationDTO>() + .ForMember(dest => dest.AvailableSeats, opt => opt.MapFrom(src => src.SeatCapacity - src.SeatReserved)); + + CreateMap<FlightInformation, BookingFlightInformationDTO>(); + + CreateMap<FlightFare, BookingFlightFareDTO>() + .ForMember(dest => dest.FlightNumber, opt => opt.MapFrom(src => src.FlightInformation.FlightNumber)); + + CreateMap<Booking, BookingDTO>() + .ForMember(dest => dest.SeatNumber, opt => opt.MapFrom(src => src.ReservedSeat != null ? src.ReservedSeat.SeatNumber : null)); + } + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Services/FlightFareService.cs b/BookingService/FlightBooking.Service/Services/FlightFareService.cs new file mode 100644 index 0000000..33bb18c --- /dev/null +++ b/BookingService/FlightBooking.Service/Services/FlightFareService.cs @@ -0,0 +1,44 @@ +using AutoMapper; +using AutoMapper.QueryableExtensions; +using FlightBooking.Service.Data; +using FlightBooking.Service.Data.DTO; +using FlightBooking.Service.Data.Models; +using FlightBooking.Service.Data.Repository; +using FlightBooking.Service.Services.Interfaces; +using Microsoft.EntityFrameworkCore; + +namespace FlightBooking.Service.Services +{ + ///<inheritdoc /> + public class FlightFareService : IFlightFareService + { + private readonly IGenericRepository<FlightFare> _fareRepository; + private readonly IMapper _mapper; + + public FlightFareService(IMapper mapper, IGenericRepository<FlightFare> fareRepository) + { + _mapper = mapper; + _fareRepository = fareRepository; + } + + public ServiceResponse<IEnumerable<FlightFareDTO>?> GetFaresByFlightNumber(string flightNumber) + { + var fares = _fareRepository.Query() + .Include(x => x.FlightInformation) + .Where(x => x.FlightInformation.FlightNumber == flightNumber) + .ProjectTo<FlightFareDTO>(_mapper.ConfigurationProvider) + .ToList(); + + return new ServiceResponse<IEnumerable<FlightFareDTO>?>(fares, InternalCode.Success); + } + + public async Task<ServiceResponse<string>> UpdateFlightFareCapacityAsync(int fareId) + { + int result = await _fareRepository.Query() + .Where(x => x.Id == fareId) + .ExecuteUpdateAsync(x => x.SetProperty(y => y.SeatReserved, y => y.SeatReserved + 1)); + + return new ServiceResponse<string>(string.Empty, (InternalCode)result); + } + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Services/FlightService.cs b/BookingService/FlightBooking.Service/Services/FlightService.cs new file mode 100644 index 0000000..f7f4e13 --- /dev/null +++ b/BookingService/FlightBooking.Service/Services/FlightService.cs @@ -0,0 +1,47 @@ +using AutoMapper; +using FlightBooking.Service.Data; +using FlightBooking.Service.Data.DTO; +using FlightBooking.Service.Data.Models; +using FlightBooking.Service.Data.Repository; +using FlightBooking.Service.Services.Interfaces; +using Microsoft.EntityFrameworkCore; + +namespace FlightBooking.Service.Services +{ + ///<inheritdoc /> + public class FlightService : IFlightService + { + private readonly IGenericRepository<FlightInformation> _flightRepo; + private readonly IMapper _mapper; + + public FlightService(IGenericRepository<FlightInformation> flightRepo, IMapper mapper) + { + _mapper = mapper; + _flightRepo = flightRepo; + } + + public async Task<ServiceResponse<FlightInformationDTO?>> GetFlightInformationAsync(string flightNumber) + { + FlightInformation? flight = await _flightRepo.Query() + .FirstOrDefaultAsync(x => x.FlightNumber == flightNumber); + + if (flight == null) + { + return new ServiceResponse<FlightInformationDTO?>(null, InternalCode.EntityNotFound, "flight not found"); + } + + FlightInformationDTO flightDto = _mapper.Map<FlightInformation, FlightInformationDTO>(flight); + + return new ServiceResponse<FlightInformationDTO?>(flightDto, InternalCode.Success); + } + + public async Task<ServiceResponse<string>> UpdateFlightCapacityAsync(string flightNumber, int bookedSeats) + { + int result = await _flightRepo.Query() + .Where(x => x.FlightNumber == flightNumber) + .ExecuteUpdateAsync(x => x.SetProperty(y => y.SeatReserved, y => y.SeatReserved + bookedSeats)); + + return new ServiceResponse<string>(string.Empty, (InternalCode)result); + } + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Services/Interfaces/IBookingOrderService.cs b/BookingService/FlightBooking.Service/Services/Interfaces/IBookingOrderService.cs new file mode 100644 index 0000000..7e1b124 --- /dev/null +++ b/BookingService/FlightBooking.Service/Services/Interfaces/IBookingOrderService.cs @@ -0,0 +1,21 @@ +using FlightBooking.Service.Data.DTO; + +namespace FlightBooking.Service.Services.Interfaces +{ + public interface IBookingOrderService + { + /// <summary> + /// Create a new order with a list of booking. Accepts one-way, two and multiple bookings per order + /// </summary> + /// <param name="order"></param> + /// <returns>Returns an object containing payment reference and order number</returns> + Task<ServiceResponse<BookingResponseDTO?>> CreateBookingOrderAsync(BookingOrderDTO order); + + /// <summary> + /// Creates a Stripe payment link using the order number to search for the particular order + /// </summary> + /// <param name="orderNumber"></param> + /// <returns>Returns an object containing payment reference and order number</returns> + Task<ServiceResponse<BookingResponseDTO?>> GetCheckoutUrlAsync(string orderNumber); + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Services/Interfaces/IBookingService.cs b/BookingService/FlightBooking.Service/Services/Interfaces/IBookingService.cs new file mode 100644 index 0000000..47c5aae --- /dev/null +++ b/BookingService/FlightBooking.Service/Services/Interfaces/IBookingService.cs @@ -0,0 +1,35 @@ +using FlightBooking.Service.Data.DTO; + +namespace FlightBooking.Service.Services.Interfaces +{ + public interface IBookingService + { + /// <summary> + /// Get booking information using booking ID + /// </summary> + /// <param name="bookingId"></param> + /// <returns></returns> + Task<ServiceResponse<BookingDTO?>> GetBookingByBookingId(int bookingId); + + /// <summary> + /// Get booking information using booking number + /// </summary> + /// <param name="bookingNumber"></param> + /// <returns></returns> + Task<ServiceResponse<BookingDTO?>> GetBookingByBookingNumberAsync(string bookingNumber); + + /// <summary> + /// Get all bookings for an email address + /// </summary> + /// <param name="email"></param> + /// <returns></returns> + ServiceResponse<IEnumerable<BookingDTO>?> GetBookingsByEmail(string email); + + /// <summary> + /// Get all booking for an order + /// </summary> + /// <param name="orderNumber"></param> + /// <returns></returns> + ServiceResponse<IEnumerable<BookingDTO>?> GetBookingsByOrderNumber(string orderNumber); + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Services/Interfaces/IFlightFareService.cs b/BookingService/FlightBooking.Service/Services/Interfaces/IFlightFareService.cs new file mode 100644 index 0000000..34e4545 --- /dev/null +++ b/BookingService/FlightBooking.Service/Services/Interfaces/IFlightFareService.cs @@ -0,0 +1,21 @@ +using FlightBooking.Service.Data.DTO; + +namespace FlightBooking.Service.Services.Interfaces +{ + public interface IFlightFareService + { + /// <summary> + /// Get all the fares for a flight using flight number + /// </summary> + /// <param name="flightNumber"></param> + /// <returns></returns> + ServiceResponse<IEnumerable<FlightFareDTO>?> GetFaresByFlightNumber(string flightNumber); + + /// <summary> + /// Update the flight capacity + /// </summary> + /// <param name="fareId"></param> + /// <returns></returns> + Task<ServiceResponse<string>> UpdateFlightFareCapacityAsync(int fareId); + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Services/Interfaces/IFlightService.cs b/BookingService/FlightBooking.Service/Services/Interfaces/IFlightService.cs new file mode 100644 index 0000000..290f5d2 --- /dev/null +++ b/BookingService/FlightBooking.Service/Services/Interfaces/IFlightService.cs @@ -0,0 +1,22 @@ +using FlightBooking.Service.Data.DTO; + +namespace FlightBooking.Service.Services.Interfaces +{ + public interface IFlightService + { + /// <summary> + /// Gets the flight information for a flight + /// </summary> + /// <param name="flightNumber"></param> + /// <returns></returns> + Task<ServiceResponse<FlightInformationDTO?>> GetFlightInformationAsync(string flightNumber); + + /// <summary> + /// Updates the flight capacity + /// </summary> + /// <param name="flightNumber"></param> + /// <param name="bookedSeats"></param> + /// <returns></returns> + Task<ServiceResponse<string>> UpdateFlightCapacityAsync(string flightNumber, int bookedSeats); + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Services/Interfaces/IReservedSeatService.cs b/BookingService/FlightBooking.Service/Services/Interfaces/IReservedSeatService.cs new file mode 100644 index 0000000..c0f7055 --- /dev/null +++ b/BookingService/FlightBooking.Service/Services/Interfaces/IReservedSeatService.cs @@ -0,0 +1,29 @@ +using FlightBooking.Service.Data.DTO; + +namespace FlightBooking.Service.Services.Interfaces +{ + public interface IReservedSeatService + { + /// <summary> + /// Gets all available seats for a flight + /// </summary> + /// <param name="flightNumber"></param> + /// <returns></returns> + ServiceResponse<IEnumerable<ReservedSeatDTO>> GetAvailableSeatsByFlightNumber(string flightNumber); + + /// <summary> + /// Creates a seat reservation for a valid booking + /// </summary> + /// <param name="requestDTO"></param> + /// <returns></returns> + Task<ServiceResponse<string>> ReserveSeatAsync(ReservedSeatRequestDTO requestDTO); + + /// <summary> + /// Generates seat numbers for a flight based on flight capacity + /// </summary> + /// <param name="flightNumber"></param> + /// <param name="flightCapacity"></param> + /// <returns></returns> + Task<ServiceResponse<string>> GenerateSeatNumbersAsync(string flightNumber, int flightCapacity); + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Services/Interfaces/IStripeService.cs b/BookingService/FlightBooking.Service/Services/Interfaces/IStripeService.cs new file mode 100644 index 0000000..abfb0f0 --- /dev/null +++ b/BookingService/FlightBooking.Service/Services/Interfaces/IStripeService.cs @@ -0,0 +1,22 @@ +using FlightBooking.Service.Data.DTO; +using Stripe; + +namespace FlightBooking.Service.Services.Interfaces +{ + public interface IStripeService + { + /// <summary> + /// Creates a Stripe payment link + /// </summary> + /// <param name="stripeDataDTO"></param> + /// <returns>Payment link</returns> + ServiceResponse<string> GetStripeCheckoutUrl(StripeDataDTO stripeDataDTO); + + /// <summary> + /// Processes Stripe events when a checkout (payment) is completed + /// </summary> + /// <param name="stripeEvent"></param> + /// <returns></returns> + Task<ServiceResponse<string>> ProcessPayment(Event stripeEvent); + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Services/PaymentService.cs b/BookingService/FlightBooking.Service/Services/PaymentService.cs new file mode 100644 index 0000000..b66d125 --- /dev/null +++ b/BookingService/FlightBooking.Service/Services/PaymentService.cs @@ -0,0 +1,6 @@ +namespace FlightBooking.Service.Services +{ + public class PaymentService + { + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Services/ReservedSeatService.cs b/BookingService/FlightBooking.Service/Services/ReservedSeatService.cs new file mode 100644 index 0000000..16126d1 --- /dev/null +++ b/BookingService/FlightBooking.Service/Services/ReservedSeatService.cs @@ -0,0 +1,127 @@ +using AutoMapper; +using AutoMapper.QueryableExtensions; +using FlightBooking.Service.Data; +using FlightBooking.Service.Data.DTO; +using FlightBooking.Service.Data.Models; +using FlightBooking.Service.Data.Repository; +using FlightBooking.Service.Services.Interfaces; +using Microsoft.EntityFrameworkCore; + +namespace FlightBooking.Service.Services +{ + ///<inheritdoc /> + public class ReservedSeatService : IReservedSeatService + { + private readonly IGenericRepository<ReservedSeat> _seatRepo; + private readonly IGenericRepository<Booking> _bookingRepo; + private readonly IMapper _mapper; + + public ReservedSeatService(IGenericRepository<ReservedSeat> seatRepository, IGenericRepository<Booking> bookingRepo, + IMapper mapper) + { + _seatRepo = seatRepository; + _bookingRepo = bookingRepo; + _mapper = mapper; + } + + public async Task<ServiceResponse<string>> ReserveSeatAsync(ReservedSeatRequestDTO requestDTO) + { + if (requestDTO == null) + { + return new ServiceResponse<string>(string.Empty, InternalCode.InvalidParam); + } + + //check if booking is valid + Booking? booking = await _bookingRepo.Query() + .Include(x => x.FlightInformation) + .FirstOrDefaultAsync(x => x.BookingNumber == requestDTO.BookingNumber); + + if (booking == null) + { + return new ServiceResponse<string>(string.Empty, InternalCode.EntityNotFound, "Booking not found for the supplied booking number"); + } + + //check if seat is available + ReservedSeat? existingSeat = await _seatRepo.Query() + .Include(x => x.FlightInformation) + .FirstOrDefaultAsync(x => x.FlightNumber == booking.FlightInformation.FlightNumber + && x.SeatNumber == requestDTO.SeatNumber); + + if (existingSeat == null) + { + return new ServiceResponse<string>(string.Empty, InternalCode.Unprocessable, "The selected seat number does not exist"); + } + + if (existingSeat.IsReserved) + { + return new ServiceResponse<string>(string.Empty, InternalCode.Unprocessable, "The selected seat has already been reserved"); + } + + //reserve the seat + existingSeat.IsReserved = true; + existingSeat.BookingNumber = booking.BookingNumber; + existingSeat.BookingId = booking.Id; + + int result = await _seatRepo.SaveChangesToDbAsync(); + + return new ServiceResponse<string>(string.Empty, (InternalCode)result); + } + + public ServiceResponse<IEnumerable<ReservedSeatDTO>> GetAvailableSeatsByFlightNumber(string flightNumber) + { + List<ReservedSeatDTO> seats = _seatRepo.Query() + .Where(x => x.FlightNumber == flightNumber && !x.IsReserved) + .ProjectTo<ReservedSeatDTO>(_mapper.ConfigurationProvider) + .ToList(); + + return new ServiceResponse<IEnumerable<ReservedSeatDTO>>(seats, InternalCode.Success); + } + + public async Task<ServiceResponse<string>> GenerateSeatNumbersAsync(string flightNumber, int flightCapacity) + { + //assume seats are in group of 4 Alphabets e.g 1A, 1B, 1C, 1D + + Dictionary<int, string> SeatMaps = new Dictionary<int, string> + { + {1, "A" }, + {2, "B" }, + {3, "C" }, + {4, "D" } + }; + + int seatId = 1; + int seatCount = 1; + + List<string> seatNumbers = new List<string>(); + + for (int i = 1; i < flightCapacity + 1; i++) + { + if (seatCount > 4) + { + seatId++; + seatCount = 1; + } + + seatNumbers.Add(seatId + SeatMaps[seatCount]); + seatCount++; + } + + List<ReservedSeat> reservedSeats = new List<ReservedSeat>(); + + foreach (var seatNumber in seatNumbers) + { + reservedSeats.Add(new ReservedSeat + { + BookingNumber = null, + FlightNumber = flightNumber, + IsReserved = false, + SeatNumber = seatNumber + }); + } + + int result = await _seatRepo.BulkCreateAsync(reservedSeats); + + return new ServiceResponse<string>(string.Empty, (InternalCode)result); + } + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Services/ResponseFormatter.cs b/BookingService/FlightBooking.Service/Services/ResponseFormatter.cs new file mode 100644 index 0000000..504c418 --- /dev/null +++ b/BookingService/FlightBooking.Service/Services/ResponseFormatter.cs @@ -0,0 +1,176 @@ +using FlightBooking.Service.Data; +using Microsoft.AspNetCore.Mvc; + +namespace FlightBooking.Service.Services +{ + public static class FormatResponseExtension + { + public static ActionResult FormatResponse<T>(this ServiceResponse<T> serviceResponse) + { + ObjectResult response; + ProblemDetails problemDetails; + + if (serviceResponse == null) + { + problemDetails = new ProblemDetails + { + Status = 500, + Title = ServiceErrorMessages.OperationFailed, + Type = "https://tools.ietf.org/html/rfc7231#section-6.6" + }; + return new ObjectResult(problemDetails) + { + StatusCode = 500 + }; + } + + switch (serviceResponse.ServiceCode) + { + case InternalCode.Failed: + problemDetails = new ProblemDetails + { + Status = 500, + Title = ServiceErrorMessages.OperationFailed, + Detail = serviceResponse.Message, + Type = "https://tools.ietf.org/html/rfc7231#section-6.6" + }; + response = new ObjectResult(problemDetails) + { + StatusCode = 500 + }; + + return response; + + case InternalCode.Success: + + if (serviceResponse.Data != null) + { + Type dataType = serviceResponse.Data.GetType(); + + if (dataType == typeof(string)) + { + string? data = serviceResponse!.Data as string; + if (string.IsNullOrEmpty(data)) + { + return new OkResult(); + } + } + } + + return new OkObjectResult(serviceResponse.Data); + + case InternalCode.UpdateError: + problemDetails = new ProblemDetails + { + Status = 500, + Title = ServiceErrorMessages.InternalServerError, + Detail = serviceResponse.Message, + Type = "https://tools.ietf.org/html/rfc7231#section-6.6" + }; + response = new ObjectResult(problemDetails) + { + StatusCode = 500, + }; + return response; + + case InternalCode.Mismatch: + problemDetails = new ProblemDetails + { + Status = 400, + Title = ServiceErrorMessages.MisMatch, + Detail = serviceResponse.Message, + Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1" + }; + return new BadRequestObjectResult(problemDetails); + + case InternalCode.EntityIsNull: + problemDetails = new ProblemDetails + { + Status = 400, + Title = ServiceErrorMessages.EntityIsNull, + Detail = serviceResponse.Message, + Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1" + }; + return new BadRequestObjectResult(problemDetails); + + case InternalCode.InvalidParam: + problemDetails = new ProblemDetails + { + Status = 400, + Title = ServiceErrorMessages.InvalidParam, + Detail = serviceResponse.Message, + Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1" + }; + return new BadRequestObjectResult(problemDetails); + + case InternalCode.EntityNotFound: + problemDetails = new ProblemDetails + { + Status = 404, + Title = ServiceErrorMessages.EntityNotFound, + Detail = string.IsNullOrEmpty(serviceResponse.Message) ? "The requested resource was not found" : serviceResponse.Message, + Type = "https://tools.ietf.org/html/rfc7231#section-6.5.4" + }; + return new NotFoundObjectResult(problemDetails); + + case InternalCode.Incompleted: + return new AcceptedResult("", serviceResponse.Data); + + case InternalCode.ListEmpty: + problemDetails = new ProblemDetails + { + Status = 400, + Title = ServiceErrorMessages.EntityIsNull, + Detail = serviceResponse.Message, + Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1" + }; + return new BadRequestObjectResult(problemDetails); + + case InternalCode.EntityExist: + problemDetails = new ProblemDetails + { + Status = 409, + Title = ServiceErrorMessages.EntityExist, + Detail = string.IsNullOrEmpty(serviceResponse.Message) ? $"An entity of the type exists" : serviceResponse.Message, + Type = "https://tools.ietf.org/html/rfc7231#section-6.5.8" + }; + return new ConflictObjectResult(problemDetails); + + case InternalCode.Unprocessable: + problemDetails = new ProblemDetails + { + Status = 422, + Title = ServiceErrorMessages.UnprocessableEntity, + Detail = string.IsNullOrEmpty(serviceResponse.Message) ? "The request cannot be processed" : serviceResponse.Message + }; + return new UnprocessableEntityObjectResult(problemDetails); + + case InternalCode.Unauthorized: + problemDetails = new ProblemDetails + { + Status = 401, + Title = "Unathorized request", + Detail = "The supplied credentials is invalid." + }; + return new UnauthorizedObjectResult(problemDetails); + + default: + return new OkObjectResult(serviceResponse.Data); + } + } + } + + public class ServiceResponse<T> + { + public InternalCode ServiceCode { get; set; } = InternalCode.Failed; + public T Data { get; set; } + public string Message { get; set; } + + public ServiceResponse(T data, InternalCode serviceCode = InternalCode.Failed, string message = "") + { + Message = message; + ServiceCode = serviceCode; + Data = data; + } + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Services/ServicesModule.cs b/BookingService/FlightBooking.Service/Services/ServicesModule.cs new file mode 100644 index 0000000..30710dc --- /dev/null +++ b/BookingService/FlightBooking.Service/Services/ServicesModule.cs @@ -0,0 +1,17 @@ +using FlightBooking.Service.Services.Interfaces; + +namespace FlightBooking.Service.Services +{ + public static class ServicesModule + { + public static void AddServices(this IServiceCollection services) + { + services.AddScoped<IBookingService, BookingService>(); + services.AddScoped<IBookingOrderService, BookingOrderService>(); + services.AddScoped<IReservedSeatService, ReservedSeatService>(); + services.AddScoped<IFlightFareService, FlightFareService>(); + services.AddScoped<IFlightService, FlightService>(); + services.AddScoped<IStripeService, StripeService>(); + } + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Services/StripeService.cs b/BookingService/FlightBooking.Service/Services/StripeService.cs new file mode 100644 index 0000000..ab75571 --- /dev/null +++ b/BookingService/FlightBooking.Service/Services/StripeService.cs @@ -0,0 +1,147 @@ +using FlightBooking.Service.Data; +using FlightBooking.Service.Data.Configs; +using FlightBooking.Service.Data.DTO; +using FlightBooking.Service.Data.Models; +using FlightBooking.Service.Data.Repository; +using FlightBooking.Service.Services.Interfaces; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using Stripe; +using Stripe.Checkout; + +namespace FlightBooking.Service.Services +{ + ///<inheritdoc /> + public class StripeService : IStripeService + { + private readonly StripeConfig _stripeConfig; + private readonly IGenericRepository<Payment> _paymentRepo; + private readonly IGenericRepository<BookingOrder> _orderRepo; + + private readonly ILogger<StripeService> _logger; + + public StripeService(IOptionsMonitor<StripeConfig> options, IGenericRepository<Payment> paymentRepo, + IGenericRepository<BookingOrder> orderRepo, ILogger<StripeService> logger) + { + _stripeConfig = options.CurrentValue; + _paymentRepo = paymentRepo; + _orderRepo = orderRepo; + _logger = logger; + } + + public ServiceResponse<string> GetStripeCheckoutUrl(StripeDataDTO stripeDataDTO) + { + if (stripeDataDTO == null) + { + return new ServiceResponse<string>(string.Empty, InternalCode.InvalidParam, "Invalid Data"); + } + + string checkoutUrl = string.Empty; + + try + { + StripeConfiguration.ApiKey = _stripeConfig.SecretKey; + + var amountInCents = stripeDataDTO.Amount * 100; + + var options = new SessionCreateOptions + { + LineItems = new List<SessionLineItemOptions> + { + new SessionLineItemOptions + { + PriceData = new SessionLineItemPriceDataOptions + { + Currency = stripeDataDTO.CurrencyCode, + UnitAmountDecimal = amountInCents, //in cents + ProductData = new SessionLineItemPriceDataProductDataOptions + { + Name = stripeDataDTO.ProductName, + Description = stripeDataDTO.ProductDescription + }, + }, + Quantity = 1, + }, + }, + Mode = "payment", + SuccessUrl = stripeDataDTO.SuccessUrl, + CancelUrl = stripeDataDTO.CancelUrl, + ClientReferenceId = stripeDataDTO.OrderNumber, + CustomerEmail = stripeDataDTO.CustomerEmail, + }; + var service = new SessionService(); + Session session = service.Create(options); + checkoutUrl = session.Url; + } + catch (Exception ex) + { + _logger.LogCritical(ex.ToString()); + } + + return new ServiceResponse<string>(checkoutUrl, InternalCode.Success); + } + + public async Task<ServiceResponse<string>> ProcessPayment(Event stripeEvent) + { + if (stripeEvent == null) + { + return new ServiceResponse<string>(string.Empty, InternalCode.InvalidParam); + } + + var session = stripeEvent.Data.Object as Session; + + //check if payment already saved, if yes, return + bool isPaymentSaved = _paymentRepo.Query() + .Any(x => x.OrderNumber == session!.ClientReferenceId); + + if (isPaymentSaved) + { + return new ServiceResponse<string>(string.Empty, InternalCode.Success); + } + + string orderNumber = session!.ClientReferenceId; + + var bookingOrder = await _orderRepo.Query() + .Include(x => x.Bookings) + .FirstOrDefaultAsync(x => x.OrderNumber == orderNumber); + + if (bookingOrder == null) + { + _logger.LogCritical("Payment made for a booking that doesn't exist"); + return new ServiceResponse<string>(string.Empty, InternalCode.Success); + } + + //save payment + Payment payment = new Payment + { + TransactionDate = session!.Created, + OrderNumber = orderNumber, + MetaData = JsonConvert.SerializeObject(session), + BookingOrderId = bookingOrder.Id, + CurrencyCode = session.Currency, + CustomerEmail = session.CustomerEmail, + PaymentReference = Guid.NewGuid().ToString("N").ToUpper(), + PaymentStatus = session.PaymentStatus, + CreatedAt = DateTime.UtcNow, + TransactionAmount = (decimal)session.AmountTotal!, + PaymentChannel = "Stripe", + }; + + await _paymentRepo.CreateAsync(payment); + + //update flight and booking information + + bookingOrder.OrderStatus = BookingStatus.Paid; + + foreach (var booking in bookingOrder.Bookings) + { + booking.BookingStatus = BookingStatus.Paid; + } + + await _orderRepo.SaveChangesToDbAsync(); + + return new ServiceResponse<string>(string.Empty, InternalCode.Success); + } + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/Startup.cs b/BookingService/FlightBooking.Service/Startup.cs new file mode 100644 index 0000000..540cfff --- /dev/null +++ b/BookingService/FlightBooking.Service/Startup.cs @@ -0,0 +1,162 @@ +using FlightBooking.Service.Data; +using FlightBooking.Service.Data.Configs; +using FlightBooking.Service.Data.Repository; +using FlightBooking.Service.Middleware; +using FlightBooking.Service.Services; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; +using Newtonsoft.Json.Converters; +using System.Text; + +namespace FlightBooking.Service +{ + public class Startup + { + public static readonly LoggerFactory _myLoggerFactory = + new LoggerFactory(new[] + { + new Microsoft.Extensions.Logging.Debug.DebugLoggerProvider() + }); + + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + //we keep using NewtonSoft so that serialization of reference loop can be ignored, especially because of EFCore + services.AddControllers() + .AddNewtonsoftJson(x => x.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore) + .AddNewtonsoftJson(x => x.SerializerSettings.Converters.Add(new StringEnumConverter())); + + services.AddAutoMapper(typeof(Startup)); + + //Configuration for SQL Servr and MySql + string mysqlConnectionString = Configuration.GetConnectionString("FlightBookingServiceDb_Mysql")!; + var mySqlServerVersion = new MySqlServerVersion(new Version(8, 0, 36)); + + services.AddDbContext<FlightBookingContext>(options => + { + //options.UseLoggerFactory(_myLoggerFactory).EnableSensitiveDataLogging(); //DEV: ENABLE TO SEE SQL Queries + + //To Use Sql Server + //options.UseSqlServer(Configuration.GetConnectionString("FlightBookingServiceDb")); + + //To Use MySql + options.UseMySql(mysqlConnectionString, mySqlServerVersion, opt => opt.EnableRetryOnFailure()) + .LogTo(Console.WriteLine, LogLevel.Warning) + .EnableSensitiveDataLogging() + .EnableDetailedErrors(); + }); + + services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options => + { + //For OpenId Connect tokens + options.Authority = Configuration["JWTConfig:Issuer"]; + options.Audience = Configuration["JWTConfig:Issuer"]; + options.SaveToken = true; + + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = false, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuers = [Configuration["JWTConfig:Issuer"], Configuration["JWTConfig:Issuer"]], + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JWTConfig:Key"]!)), + }; + }); + + services.AddCors(option => + { + option.AddDefaultPolicy( + builder => + { + builder + .AllowAnyOrigin() + .SetIsOriginAllowedToAllowWildcardSubdomains() + .AllowAnyHeader() + .AllowAnyMethod(); + }); + }); + + services.AddSignalR(); + services.AddHttpClient(); + services.AddHttpContextAccessor(); + + //Add our custom services + services.AddRepository(); + services.AddServices(); + + services.AddEndpointsApiExplorer(); + services.AddSwaggerGen(options => + { + options.SwaggerDoc("v1", new OpenApiInfo { Title = "Flight Booking API", Version = "v1" }); + }); + + //Add our configs + services.AddConfigSettings(Configuration); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + //https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-8.0 + //The following Startup.Configure method adds middleware components for common app scenarios: + + //1. Exception / error handling (HTTP Strict Transport Security Protocol in prod) + //2. HTTPS redirection + //3. Static File Middleware + //4. Cookie Policy Middleware + //5. Routing Middleware (UseRouting) to route requests. + //5. Cors + //5. Custom route + //6. Authentication Middleware + //7. Authorization Middleware + //8. Session Middleware + //9. Endpoint Routing Middleware (UseEndpoints + + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + //app.UseDatabaseErrorPage(); + } + else + { + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); + app.UseErrorHandlingMiddleware(); //custom error handler + //app.UseExceptionHandler(); + } + + app.UseHttpsRedirection(); + + app.UseRouting(); + + app.UseCors();//this was moved here since the BasicAuthMiddleware below is also authentication and cors must come before authentication. + + if (env.IsDevelopment() || env.IsStaging()) + { + app.UseSwagger(); + app.UseSwaggerUI(opt => + { + opt.SwaggerEndpoint("/swagger/v1/swagger.json", "Flight Booking API"); + }); + } + + app.UseAuthentication(); + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + } + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/appsettings.Development.json b/BookingService/FlightBooking.Service/appsettings.Development.json new file mode 100644 index 0000000..14d2ada --- /dev/null +++ b/BookingService/FlightBooking.Service/appsettings.Development.json @@ -0,0 +1,21 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "ConnectionStrings": { + "FlightBookingServiceDb": "Server=.;Database=FlightBooking_Db;User ID=sa;Password=beforwardj!;Trusted_Connection=False;Encrypt=False;Connection Timeout=180;", + "FlightBookingServiceDb_Mysql": "Server=localhost;Database=FlightBooking_Db;User=root;Password=P@ss1ord" + }, + "JWTConfig": { + "Key": "28298659-1c10-4f2e-b045-42698ab4b02b", + "Issuer": "https://localhost:44321" + }, + "StripeConfig": { + "PublicKey": "", + "SecretKey": "", + "SigningSecret": "" + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/appsettings.json b/BookingService/FlightBooking.Service/appsettings.json new file mode 100644 index 0000000..31a3e8d --- /dev/null +++ b/BookingService/FlightBooking.Service/appsettings.json @@ -0,0 +1,22 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "FlightBookingServiceDb": "Server=.;Database=FlightBooking_Db;User ID=sa;Password=beforwardj!;Trusted_Connection=False;Encrypt=False;Connection Timeout=180;", + "FlightBookingServiceDb_Mysql": "Server=localhost;Database=FlightBooking_Db;User=root;Password=P@ss1ord" + }, + "JWTConfig": { + "Key": "28298659-1c10-4f2e-b045-42698ab4b02b", + "Issuer": "https://localhost:44321" + }, + "StripeConfig": { + "PublicKey": "", + "SecretKey": "", + "SigningSecret": "" + } +} \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/nlog.config b/BookingService/FlightBooking.Service/nlog.config new file mode 100644 index 0000000..1967321 --- /dev/null +++ b/BookingService/FlightBooking.Service/nlog.config @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="utf-8" ?> +<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + autoReload="true" + internalLogLevel="Info" + internalLogFile=".\log\internal-nlog.txt"> + + <!-- enable asp.net core layout renderers --> + <extensions> + <add assembly="NLog.Web.AspNetCore" /> + </extensions> + + <!-- the targets to write to --> + <targets> + <!-- write logs to file --> + <target xsi:type="File" name="allfile" fileName=".\log\nlog-all-${shortdate}.log" + layout="${longdate}|${event-properties:item=EventId_Id:whenEmpty=0}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}" /> + <!-- another file log, only own logs. Uses some ASP.NET core renderers --> + <target xsi:type="File" name="ownFile-web" fileName=".\log\nlog-own-${shortdate}.log" + layout="${longdate}|${event-properties:item=EventId_Id:whenEmpty=0}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}|url: ${aspnet-request-url}|action: ${aspnet-mvc-action}|${callsite}| body: ${aspnet-request-posted-body}" /> + + <!--Console Target for hosting lifetime messages to improve Docker / Visual Studio startup detection --> + <target xsi:type="Console" name="lifetimeConsole" layout="${level:truncate=4:lowercase=true}: ${logger}[0]${newline} ${message}${exception:format=tostring}" /> + </targets> + + <!-- rules to map from logger name to target --> + <rules> + <!--All logs, including from Microsoft--> + <logger name="*" minlevel="Trace" writeTo="allfile" /> + + <!--Output hosting lifetime messages to console target for faster startup detection --> + <logger name="Microsoft.Hosting.Lifetime" minlevel="Info" writeTo="lifetimeConsole, ownFile-web" final="true" /> + + <!--Skip non-critical Microsoft logs and so log only own logs (BlackHole) --> + <logger name="Microsoft.*" maxlevel="Info" final="true" /> + <logger name="System.Net.Http.*" maxlevel="Info" final="true" /> + + <logger name="*" minlevel="Trace" writeTo="ownFile-web" /> + </rules> +</nlog> \ No newline at end of file diff --git a/BookingService/FlightBooking.Service/readme.md b/BookingService/FlightBooking.Service/readme.md new file mode 100644 index 0000000..77617ad --- /dev/null +++ b/BookingService/FlightBooking.Service/readme.md @@ -0,0 +1,385 @@ +Libraries Used: + +1. NLog. We used NLog for logging. By default all logs with level Informational are saved. This setting can be changed in the appsettings.json + +nlog.config contains the file that is used to configure our NLog. + +2. Automapper: This is a great utility tool that allows mapping one model or DTO (Data Transfer Object) to another. It helps to us avoid repetitve code. + To use, we first add a line in the `ConfigureServices` method of our `Startup.cs`: + + `services.AddAutoMapper(typeof(Startup));` + + The we declare a class that contains our mappings: + ``` + public class FlightBookingProfile : Profile + { + public FlightBookingProfile() + { + CreateMap<FlightFare, FlightFareDTO>() + .ForMember(dest => dest.AvailableSeats, opt => opt.MapFrom(src => src.SeatCapacity - src.SeatReserved)) + .ForMember(dest => dest.FlightNumber, opt => opt.MapFrom(src => src.FlightInformation.FlightNumber)); + } + } + ``` + + +3. We also installed NewtonSoftJson library for working with Json input and output. + +4. Swashbuckle was also installed so we can generate Swagger documentation from our controllers. + +5. Some libraries for using Jwt for authentication were also added. + + +## Program Flow. + +Controller -> Services -> Repository -> EntityFramework -> Data + + +### Controllers + +They represent our endpoints based on the MVC pattern. +Each controller has been decorated with attributes that makes it easy to read what input and output to expect. +For example, our `ReservedSeatController`: + +``` + [Route("api/[controller]")] + [ApiController] + public class SeatsController : ControllerBase + { + private readonly IReservedSeatService _service; + + public SeatsController(IReservedSeatService service) + { + _service = service; + } + + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<ReservedSeatDTO>))] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ProblemDetails))] + public IActionResult GetAvailableSeats([FromQuery] string flightNumber) + { + ServiceResponse<IEnumerable<ReservedSeatDTO>> result = _service.GetAvailableSeatsByFlightNumber(flightNumber); + + return result.FormatResponse(); + } + + [HttpPost] + [Consumes(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity, Type = typeof(ProblemDetails))] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ProblemDetails))] + public async Task<IActionResult> ReserveSeat([FromBody] ReservedSeatRequestDTO requestDTO) + { + ServiceResponse<string> result = await _service.ReserveSeatAsync(requestDTO); + + return result.FormatResponse(); + } + } +``` + +In the above, we have to methods in the controller which corresponds to 2 endpoints. + +First, we inject our `IReservedSeatService` into the controller. + +The first endpoint is a GET endpoint: +And the attributes show that it can return a 200, 400 and 422 response. The 400 and 422 responses are in types of ProblemDetails which is a specification for returning API responses. + +This part: `([FromQuery] string flightNumber)` indicates that a query string named ``flightNumber`` is passed to the endpoint generated by this method. + +The `_service` returns a type of `ServiceResponse` which is then formatted to return the appropriate response. More on this later + + +The second endpoint is a POST endpoint: +The attributes show it accepts a body (`[FromBody]`) of type application/json (`MediaTypeNames.Application.Json`) and response produced are 200, 422, 400. + + + +### Services +They represent the logic for our app. We this pattern to make it easy to test the services. +By abstracting the services to use Interfaces, we can easily write tests that can be flexible. + +We rely on the built-in Dependency Injection framework to resolve service dependencies. + +In each service, we inject Repository classes and other Services. + +The services also have to be registered and we do this by declaring a static class that will do the registration: + +``` +public static class ServicesModule +{ + public static void AddServices(this IServiceCollection services) + { + services.AddScoped<IBookingService, BookingService>(); + services.AddScoped<IBookingOrderService, BookingOrderService>(); + services.AddScoped<IReservedSeatService, ReservedSeatService>(); + services.AddScoped<IFlightFareService, FlightFareService>(); + services.AddScoped<IFlightService, FlightService>(); + services.AddScoped<IStripeService, StripeService>(); + } +} +``` +Here: We register each service as a Scoped dependency. Scoped means that the service will only be active for the duration of a request (i.e Http Request). + +Then we call the method in our Startup.cs: +` services.AddServices();` + +Each service also returns a type `ServiceResponse<T>` where T is a class + +The `ServiceResponse<T>` is declared in the `ResponseFormatter.cs` + +``` +public class ServiceResponse<T> +{ + public InternalCode ServiceCode { get; set; } = InternalCode.Failed; + public T Data { get; set; }// = null; + public string Message { get; set; } + + public ServiceResponse(T data, InternalCode serviceCode = InternalCode.Failed, string message = "") + { + Message = message; + ServiceCode = serviceCode; + Data = data; + } +} +``` +We declare the `ServiceResponse` to be a generic class. It's properties are: +`InternalCode` that indicates the status. InternalCode is an enum +`Data`: which is a type of `T` +`Message`: optional message + +The `FormatResponse` is an extension method that accepts a `ServiceResponse<T>` and then checks the Internal Code and uses that to +return an appropriate response. + +This makes it easy for us to return a uniform type of response. +2xx responses return a simple 2xx and an optional data +4xx and 5xx responses return a type of `ProblemDetails`. +The helps to give more context to the nature of the response. + + +### Repository + +We use a Repository pattern that wraps EntityFramework unit of work pattern. The class is declared as a Generic class so that we can pass any model to it. +We then declare helper methods that in turn call EntityFramework methods: + +``` + public class GenericRepository<T> : IGenericRepository<T> where T : class, new() + { + //Responses: failed=0, success=1 + + //IEnumerable iterates over an in-memory collection while IQueryable does so on the DB + // call to .ToList to enable instant query against DB + + protected FlightBookingContext _db; + protected ILogger _logger; + + //...omitted for brevity + + public async Task<T?> GetByGuidAsync(Guid id) + { + var entity = await _db.Set<T>().FindAsync(id); + return entity; + } + +} +``` + +In the aboved, we pass a type of `DbContext` and a `Logger`. +In the `GetByGuidAsync()` method, we use the `_db` of that model to find the data. + +We must also inject the Repository of each model so we can use anywhere in our project: +``` +public static class RepositoryModule +{ + public static void AddRepository(this IServiceCollection services) + { + services.AddScoped<IGenericRepository<Booking>, GenericRepository<Booking>>(); + services.AddScoped<IGenericRepository<FlightInformation>, GenericRepository<FlightInformation>>(); + services.AddScoped<IGenericRepository<FlightFare>, GenericRepository<FlightFare>>(); + services.AddScoped<IGenericRepository<Payment>, GenericRepository<Payment>>(); + services.AddScoped<IGenericRepository<BookingOrder>, GenericRepository<BookingOrder>>(); + services.AddScoped<IGenericRepository<ReservedSeat>, GenericRepository<ReservedSeat>>(); + } +} +``` +Just like in the Services, we also add the dependencies as a Scoped service. + +This is important because we want to quickly use a DbContext which in turn holds a connection to the database. If we use it and quickly return it to the connection pool, we can avoid issues with resource exhaustion. + + +### Data + +We are using MySql. So we must install the following MySqlConnector and the Pomelo.EntityFramework.MySql libraries. +Let's add these 2 lines to our Package reference. + +1. `<PackageReference Include="MySqlConnector" Version="2.3.5" />` +2. `<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.1" />` + + +To use our model, we use classes to represent our data. Each class in the model is used to create tables in the database. + +We declare properties of the class and also configure relationships. + +The models are in `Model` folder. + +We also wish to do some relations that are not automatically done by the framework. + +These configurations are applied to the model and is used to configure how the tables should be created during table creation: +e.g: +``` +public class BookingConfiguration : IEntityTypeConfiguration<Booking> + { + public void Configure(EntityTypeBuilder<Booking> entity) + { + entity.HasOne(d => d.BookingOrder).WithMany(p => p.Bookings) + .HasPrincipalKey(p => p.Id) + .HasForeignKey(d => d.BookingOrderId) + .OnDelete(DeleteBehavior.ClientSetNull); + + entity.HasOne(d => d.FlightInformation).WithMany(p => p.Bookings) + .HasPrincipalKey(p => p.Id) + .HasForeignKey(d => d.FlightId) + .OnDelete(DeleteBehavior.ClientSetNull); + + entity.HasOne(d => d.FlightFare).WithMany(p => p.Bookings) + .HasPrincipalKey(p => p.Id) + .HasForeignKey(d => d.FlightFareId) + .OnDelete(DeleteBehavior.ClientSetNull); + } + } +``` + The above code for the `Booking.cs` model configures the foreign key relationship. + + +To make use of our models, we create a `FlightBookingDbContext.cs` where we declare all our models and also apply the configurations: + +We also added a change in the `OnModelCreating()` method of the `FlightDbContext` to make sure that when a model (data in the database) is updated, the `UpdatedAt` and `CreatedAt` is saved as `UTC`. + +``` +foreach (var entityType in modelBuilder.Model.GetEntityTypes()) + { + foreach (var property in entityType.GetProperties()) + { + if (property.ClrType == typeof(DateTime)) + { + modelBuilder.Entity(entityType.ClrType) + .Property<DateTime>(property.Name) + .HasConversion( + v => v.ToUniversalTime(), + v => DateTime.SpecifyKind(v, DateTimeKind.Utc)); + } + else if (property.ClrType == typeof(DateTime?)) + { + modelBuilder.Entity(entityType.ClrType) + .Property<DateTime?>(property.Name) + .HasConversion( + v => v.HasValue ? v.Value.ToUniversalTime() : v, + v => v.HasValue ? DateTime.SpecifyKind(v.Value, DateTimeKind.Utc) : v); + } + } + } +``` + + +To get our database schema updated: + +Once we have configured our models, and added the `FlightBookingContext`. +We also configure the `FlightBookingContext` in the `ConfigureService` method in our `Startup.cs` and initialize it. + +``` + string mysqlConnectionString = Configuration.GetConnectionString("FlightBookingServiceDb_Mysql")!; + var mySqlServerVersion = new MySqlServerVersion(new Version(8, 0, 36)); + services.AddDbContext<FlightBookingContext>(options => + { + options.UseMySql(mysqlConnectionString, mySqlServerVersion, opt => opt.EnableRetryOnFailure()) + .LogTo(Console.WriteLine, LogLevel.Warning) + .EnableSensitiveDataLogging() + .EnableDetailedErrors(); + }); + +``` +The code indicates that we get our connection string from appsettings.json +To get the version of MySql, run this on the MySql server: + +> SELECT VERSION(); + +Once we have our models, configuration and DbContext ready, we need to run Migrations. + +Migrations take a snapshot of our models and configuration and defines how they will be used to updated the database schema at that point in time. + +To run migrations, open the Package Manager Console and run: + +> Add-Migration InitialCreate + +This will create a migration named `InitialCreateMysql`. + +We then run + +>Update-Database + +This will configure the database with the code generated in `InitialCreateMysql`. + +Whenever we make a change to our models and configuration, we must create a new migration and update our database so that the database schema is kept updated + +If we wish to use a different database, we first configure the DbContext. E.g if using Sql Server: + +``` + services.AddDbContext<FlightBookingContext>(options => + { + options.UseSqlServer(Configuration.GetConnectionString("FlightBookingServiceDb")); + }); + +``` + +Then we must create a new migration. __It is important to note that Migrations are scoped to a database. So we need a new migration when we switched to a different database__ + +To run migrations, open the Package Manager Console and run: + +> Add-Migration InitialCreateMysql + +This will create a migration named `InitialCreateMysql`. + +We then run + +>Update-Database + + +The `ConfigSettings` contains a strongly type mapping of the content of appsettings.json and helps to avoid errors + +Our DTO folder contains DTOs for models. These are basically data structure we use to transfer data around + + +## Running this Project + +1. Open the solution in Visual Studio. Automatically, nugets are installed. + +2. Start your MySql Server. The project currently uses MySql version 8.0.36. If you wish to use a different version, update the version in ``Startup.cs` + +``` + var mySqlServerVersion = new MySqlServerVersion(new Version(8, 0, 36)); +``` + +3. Add your database connection string in appsettings.json and appsettings.Development.json + +3. In Visual Studio, go to Tools -> Nuget Package Manager -> Package Manager Console. Click the Package Manager Console and it open at the bottom + +4. In the Package Manager Console, Run + > Update-Database + + The Database and all tables will be created using the `InitialMysqlMigration` that is included in the Migrations folder. + +5. Press F5 or run the project by clicking the play button. + +6. Included in the project in the `Program.cs` is a method that seeds the database with some default data. The `DatabaseSeeding.cs` contains the code that adds `FlightInformation`, `FlightFares` and `ReservedSeats` + Once you run the project for the first time, if all goes well, the data is added to the database. + + + +Useful Links: +https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql +https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/providers?tabs=dotnet-core-cli +https://learn.microsoft.com/en-us/ef/core/modeling/relationships +https://learn.microsoft.com/en-us/ef/ef6/fundamentals/working-with-dbcontext + + + + -- GitLab