diff --git a/App/App.csproj b/App/App.csproj index 12d0ea7..106d50b 100644 --- a/App/App.csproj +++ b/App/App.csproj @@ -86,5 +86,9 @@ + + + + diff --git a/App/Config/settings.example.yaml b/App/Config/settings.example.yaml index a7999d1..f55e85e 100644 --- a/App/Config/settings.example.yaml +++ b/App/Config/settings.example.yaml @@ -10,12 +10,6 @@ Logging: Console: LogLevel: Default: Information - FormatterName: json - FormatterOptions: - SingleLine: false - IncludeScopes: true - JsonWriterOptions: - Indented: true OpenTelemetry: IncludeFormattedMessage: true IncludeScopes: true diff --git a/App/Config/settings.yaml b/App/Config/settings.yaml index c2b9467..259222b 100644 --- a/App/Config/settings.yaml +++ b/App/Config/settings.yaml @@ -7,14 +7,14 @@ Kestrel: Url: http://+:9001 Logging: LogLevel: - Default: None - Console: - LogLevel: - Default: Information + Default: Information + # Console: + # LogLevel: + # Default: Debug OpenTelemetry: - IncludeFormattedMessage: true - IncludeScopes: true - ParseStateValues: true + IncludeFormattedMessage: false + IncludeScopes: false + ParseStateValues: false # Domain App: Landscape: lapras diff --git a/App/Migrations/20231123145755_InitialCreate.Designer.cs b/App/Migrations/20231123145755_InitialCreate.Designer.cs deleted file mode 100644 index 445c508..0000000 --- a/App/Migrations/20231123145755_InitialCreate.Designer.cs +++ /dev/null @@ -1,46 +0,0 @@ -// -using App.StartUp.Database; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace App.Migrations -{ - [DbContext(typeof(MainDbContext))] - [Migration("20231123145755_InitialCreate")] - partial class InitialCreate - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.0") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("App.Modules.Users.Data.UserData", b => - { - b.Property("Id") - .HasColumnType("text"); - - b.Property("Username") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("Username") - .IsUnique(); - - b.ToTable("Users"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/App/Migrations/20231123145755_InitialCreate.cs b/App/Migrations/20231123145755_InitialCreate.cs deleted file mode 100644 index 14cfa97..0000000 --- a/App/Migrations/20231123145755_InitialCreate.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace App.Migrations -{ - /// - public partial class InitialCreate : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Users", - columns: table => new - { - Id = table.Column(type: "text", nullable: false), - Username = table.Column(type: "text", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Users", x => x.Id); - }); - - migrationBuilder.CreateIndex( - name: "IX_Users_Username", - table: "Users", - column: "Username", - unique: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Users"); - } - } -} diff --git a/App/Migrations/20231202115729_InitialCreate.Designer.cs b/App/Migrations/20231202115729_InitialCreate.Designer.cs new file mode 100644 index 0000000..4ebe725 --- /dev/null +++ b/App/Migrations/20231202115729_InitialCreate.Designer.cs @@ -0,0 +1,222 @@ +// +using System; +using App.StartUp.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace App.Migrations +{ + [DbContext(typeof(MainDbContext))] + [Migration("20231202115729_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("App.Modules.Bookings.Data.BookingData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("smallint") + .HasDefaultValue((byte)0); + + b.Property("Time") + .HasColumnType("time without time zone"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Bookings"); + }); + + modelBuilder.Entity("App.Modules.Passengers.Data.PassengerData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FullName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Gender") + .HasColumnType("smallint"); + + b.Property("PassportExpiry") + .HasColumnType("date"); + + b.Property("PassportNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "PassportNumber") + .IsUnique(); + + b.ToTable("Passengers"); + }); + + modelBuilder.Entity("App.Modules.Schedules.Data.ScheduleData", b => + { + b.Property("Date") + .HasColumnType("date"); + + b.Property("Confirmed") + .HasColumnType("boolean"); + + b.Property("JToWExcluded") + .IsRequired() + .HasColumnType("time without time zone[]"); + + b.Property("WToJExcluded") + .IsRequired() + .HasColumnType("time without time zone[]"); + + b.HasKey("Date"); + + b.ToTable("Schedules"); + }); + + modelBuilder.Entity("App.Modules.Timings.Data.TimingData", b => + { + b.Property("Direction") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Direction")); + + b.Property("Timings") + .IsRequired() + .HasColumnType("time without time zone[]"); + + b.HasKey("Direction"); + + b.ToTable("Timings"); + + b.HasData( + new + { + Direction = 1, + Timings = new[] { new TimeOnly(5, 0, 0), new TimeOnly(5, 30, 0), new TimeOnly(6, 0, 0), new TimeOnly(6, 30, 0), new TimeOnly(7, 0, 0), new TimeOnly(7, 30, 0), new TimeOnly(8, 45, 0), new TimeOnly(10, 0, 0), new TimeOnly(11, 30, 0), new TimeOnly(12, 45, 0), new TimeOnly(14, 0, 0), new TimeOnly(15, 15, 0), new TimeOnly(16, 30, 0), new TimeOnly(17, 45, 0), new TimeOnly(19, 0, 0), new TimeOnly(20, 15, 0), new TimeOnly(21, 30, 0), new TimeOnly(22, 45, 0) } + }, + new + { + Direction = 2, + Timings = new[] { new TimeOnly(8, 30, 0), new TimeOnly(9, 45, 0), new TimeOnly(11, 0, 0), new TimeOnly(12, 30, 0), new TimeOnly(13, 45, 0), new TimeOnly(15, 0, 0), new TimeOnly(16, 15, 0), new TimeOnly(17, 30, 0), new TimeOnly(18, 45, 0), new TimeOnly(20, 0, 0), new TimeOnly(21, 15, 0), new TimeOnly(22, 30, 0), new TimeOnly(23, 45, 0) } + }); + }); + + modelBuilder.Entity("App.Modules.Users.Data.UserData", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("App.Modules.Bookings.Data.BookingData", b => + { + b.HasOne("App.Modules.Users.Data.UserData", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsMany("App.Modules.Bookings.Data.BookingPassengerData", "Passengers", b1 => + { + b1.Property("BookingDataId") + .HasColumnType("uuid"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b1.Property("FullName") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Gender") + .HasColumnType("smallint"); + + b1.Property("PassportExpiry") + .HasColumnType("date"); + + b1.Property("PassportNumber") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("BookingDataId", "Id"); + + b1.ToTable("Bookings"); + + b1.ToJson("Passengers"); + + b1.WithOwner() + .HasForeignKey("BookingDataId"); + }); + + b.Navigation("Passengers"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("App.Modules.Passengers.Data.PassengerData", b => + { + b.HasOne("App.Modules.Users.Data.UserData", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/App/Migrations/20231202115729_InitialCreate.cs b/App/Migrations/20231202115729_InitialCreate.cs new file mode 100644 index 0000000..c96f235 --- /dev/null +++ b/App/Migrations/20231202115729_InitialCreate.cs @@ -0,0 +1,148 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace App.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Schedules", + columns: table => new + { + Date = table.Column(type: "date", nullable: false), + Confirmed = table.Column(type: "boolean", nullable: false), + JToWExcluded = table.Column(type: "time without time zone[]", nullable: false), + WToJExcluded = table.Column(type: "time without time zone[]", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Schedules", x => x.Date); + }); + + migrationBuilder.CreateTable( + name: "Timings", + columns: table => new + { + Direction = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Timings = table.Column(type: "time without time zone[]", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Timings", x => x.Direction); + }); + + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + Username = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Bookings", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "NOW()"), + Status = table.Column(type: "smallint", nullable: false, defaultValue: (byte)0), + CompletedAt = table.Column(type: "timestamp with time zone", nullable: true), + Date = table.Column(type: "date", nullable: false), + Time = table.Column(type: "time without time zone", nullable: false), + UserId = table.Column(type: "text", nullable: false), + Passengers = table.Column(type: "jsonb", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Bookings", x => x.Id); + table.ForeignKey( + name: "FK_Bookings_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Passengers", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + FullName = table.Column(type: "text", nullable: false), + Gender = table.Column(type: "smallint", nullable: false), + PassportExpiry = table.Column(type: "date", nullable: false), + PassportNumber = table.Column(type: "text", nullable: false), + UserId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Passengers", x => x.Id); + table.ForeignKey( + name: "FK_Passengers_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.InsertData( + table: "Timings", + columns: new[] { "Direction", "Timings" }, + values: new object[,] + { + { 1, new[] { new TimeOnly(5, 0, 0), new TimeOnly(5, 30, 0), new TimeOnly(6, 0, 0), new TimeOnly(6, 30, 0), new TimeOnly(7, 0, 0), new TimeOnly(7, 30, 0), new TimeOnly(8, 45, 0), new TimeOnly(10, 0, 0), new TimeOnly(11, 30, 0), new TimeOnly(12, 45, 0), new TimeOnly(14, 0, 0), new TimeOnly(15, 15, 0), new TimeOnly(16, 30, 0), new TimeOnly(17, 45, 0), new TimeOnly(19, 0, 0), new TimeOnly(20, 15, 0), new TimeOnly(21, 30, 0), new TimeOnly(22, 45, 0) } }, + { 2, new[] { new TimeOnly(8, 30, 0), new TimeOnly(9, 45, 0), new TimeOnly(11, 0, 0), new TimeOnly(12, 30, 0), new TimeOnly(13, 45, 0), new TimeOnly(15, 0, 0), new TimeOnly(16, 15, 0), new TimeOnly(17, 30, 0), new TimeOnly(18, 45, 0), new TimeOnly(20, 0, 0), new TimeOnly(21, 15, 0), new TimeOnly(22, 30, 0), new TimeOnly(23, 45, 0) } } + }); + + migrationBuilder.CreateIndex( + name: "IX_Bookings_UserId", + table: "Bookings", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_Passengers_UserId_PassportNumber", + table: "Passengers", + columns: new[] { "UserId", "PassportNumber" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Users_Username", + table: "Users", + column: "Username", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Bookings"); + + migrationBuilder.DropTable( + name: "Passengers"); + + migrationBuilder.DropTable( + name: "Schedules"); + + migrationBuilder.DropTable( + name: "Timings"); + + migrationBuilder.DropTable( + name: "Users"); + } + } +} diff --git a/App/Migrations/MainDbContextModelSnapshot.cs b/App/Migrations/MainDbContextModelSnapshot.cs index dff5783..ddfe063 100644 --- a/App/Migrations/MainDbContextModelSnapshot.cs +++ b/App/Migrations/MainDbContextModelSnapshot.cs @@ -1,4 +1,5 @@ // +using System; using App.StartUp.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -21,6 +22,124 @@ protected override void BuildModel(ModelBuilder modelBuilder) NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + modelBuilder.Entity("App.Modules.Bookings.Data.BookingData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("smallint") + .HasDefaultValue((byte)0); + + b.Property("Time") + .HasColumnType("time without time zone"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Bookings"); + }); + + modelBuilder.Entity("App.Modules.Passengers.Data.PassengerData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FullName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Gender") + .HasColumnType("smallint"); + + b.Property("PassportExpiry") + .HasColumnType("date"); + + b.Property("PassportNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "PassportNumber") + .IsUnique(); + + b.ToTable("Passengers"); + }); + + modelBuilder.Entity("App.Modules.Schedules.Data.ScheduleData", b => + { + b.Property("Date") + .HasColumnType("date"); + + b.Property("Confirmed") + .HasColumnType("boolean"); + + b.Property("JToWExcluded") + .IsRequired() + .HasColumnType("time without time zone[]"); + + b.Property("WToJExcluded") + .IsRequired() + .HasColumnType("time without time zone[]"); + + b.HasKey("Date"); + + b.ToTable("Schedules"); + }); + + modelBuilder.Entity("App.Modules.Timings.Data.TimingData", b => + { + b.Property("Direction") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Direction")); + + b.Property("Timings") + .IsRequired() + .HasColumnType("time without time zone[]"); + + b.HasKey("Direction"); + + b.ToTable("Timings"); + + b.HasData( + new + { + Direction = 1, + Timings = new[] { new TimeOnly(5, 0, 0), new TimeOnly(5, 30, 0), new TimeOnly(6, 0, 0), new TimeOnly(6, 30, 0), new TimeOnly(7, 0, 0), new TimeOnly(7, 30, 0), new TimeOnly(8, 45, 0), new TimeOnly(10, 0, 0), new TimeOnly(11, 30, 0), new TimeOnly(12, 45, 0), new TimeOnly(14, 0, 0), new TimeOnly(15, 15, 0), new TimeOnly(16, 30, 0), new TimeOnly(17, 45, 0), new TimeOnly(19, 0, 0), new TimeOnly(20, 15, 0), new TimeOnly(21, 30, 0), new TimeOnly(22, 45, 0) } + }, + new + { + Direction = 2, + Timings = new[] { new TimeOnly(8, 30, 0), new TimeOnly(9, 45, 0), new TimeOnly(11, 0, 0), new TimeOnly(12, 30, 0), new TimeOnly(13, 45, 0), new TimeOnly(15, 0, 0), new TimeOnly(16, 15, 0), new TimeOnly(17, 30, 0), new TimeOnly(18, 45, 0), new TimeOnly(20, 0, 0), new TimeOnly(21, 15, 0), new TimeOnly(22, 30, 0), new TimeOnly(23, 45, 0) } + }); + }); + modelBuilder.Entity("App.Modules.Users.Data.UserData", b => { b.Property("Id") @@ -37,6 +156,63 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Users"); }); + + modelBuilder.Entity("App.Modules.Bookings.Data.BookingData", b => + { + b.HasOne("App.Modules.Users.Data.UserData", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsMany("App.Modules.Bookings.Data.BookingPassengerData", "Passengers", b1 => + { + b1.Property("BookingDataId") + .HasColumnType("uuid"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b1.Property("FullName") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Gender") + .HasColumnType("smallint"); + + b1.Property("PassportExpiry") + .HasColumnType("date"); + + b1.Property("PassportNumber") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("BookingDataId", "Id"); + + b1.ToTable("Bookings"); + + b1.ToJson("Passengers"); + + b1.WithOwner() + .HasForeignKey("BookingDataId"); + }); + + b.Navigation("Passengers"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("App.Modules.Passengers.Data.PassengerData", b => + { + b.HasOne("App.Modules.Users.Data.UserData", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); #pragma warning restore 612, 618 } } diff --git a/App/Modules/Bookings/API/V1/BookingController.cs b/App/Modules/Bookings/API/V1/BookingController.cs index 0c4f37b..cbea8db 100644 --- a/App/Modules/Bookings/API/V1/BookingController.cs +++ b/App/Modules/Bookings/API/V1/BookingController.cs @@ -17,10 +17,10 @@ namespace App.Modules.Bookings.API.V1; [Consumes(MediaTypeNames.Application.Json)] [Route("api/v{version:apiVersion}/[controller]")] public class BookingController( - ITrainBookingService service, + IBookingService service, CreateBookingReqValidator createBookingReqValidator, BookingSearchQueryValidator bookingSearchQueryValidator, - AuthHelper authHelper + IAuthHelper authHelper ) : AtomiControllerBase(authHelper) { [Authorize, HttpGet] @@ -60,13 +60,24 @@ public async Task>> CountStatus() return this.ReturnResult(x); } + [Authorize(Policy = AuthPolicies.OnlyAdmin), HttpPost("bypass/{userId}")] + public async Task> Create(string userId, [FromBody] CreateBookingReq req) + { + var x = await createBookingReqValidator.ValidateAsyncResult(req, "Invalid CreateBookingReq") + .ThenAwait(x => service.Create(userId, x.ToRecord())) + .Then(x => x.ToRes(), Errors.MapAll); + return this.ReturnResult(x); + } - + [Authorize(Policy = AuthPolicies.OnlyAdmin), HttpPost("cancel/bypass/{id:guid}")] + public async Task> Cancel(Guid id) + { + var x = await service.Cancel(id) + .Then(x => x?.ToRes(), Errors.MapAll); + return this.ReturnNullableResult(x, new EntityNotFound("Booking not found", typeof(Booking), id.ToString())); + } //TODO: CANCEL //TODO: CREATE - - - } diff --git a/App/Modules/Bookings/Data/BookingData.cs b/App/Modules/Bookings/Data/BookingData.cs index 5053e2c..a604539 100644 --- a/App/Modules/Bookings/Data/BookingData.cs +++ b/App/Modules/Bookings/Data/BookingData.cs @@ -1,9 +1,8 @@ -using System.ComponentModel.DataAnnotations.Schema; using App.Modules.Users.Data; namespace App.Modules.Bookings.Data; -public record BookingPassengerData +public class BookingPassengerData { public required string FullName { get; set; } @@ -30,8 +29,7 @@ public class BookingData public TimeOnly Time { get; set; } - [Column(TypeName = "jsonb")] - public BookingPassengerData[] Passengers { get; set; } = null!; + public List Passengers { get; set; } = null!; // FK diff --git a/App/Modules/Bookings/Data/BookingMapper.cs b/App/Modules/Bookings/Data/BookingMapper.cs index 8d80188..48bf58c 100644 --- a/App/Modules/Bookings/Data/BookingMapper.cs +++ b/App/Modules/Bookings/Data/BookingMapper.cs @@ -59,7 +59,7 @@ public static BookingData UpdateData(this BookingData data, BookingRecord record data.Time = record.Time; data.Passengers = record.Passengers .Select(passenger => passenger.ToData()) - .ToArray(); + .ToList(); return data; } diff --git a/App/Modules/Bookings/Data/BookingRepository.cs b/App/Modules/Bookings/Data/BookingRepository.cs index 11b69a7..4b1717f 100644 --- a/App/Modules/Bookings/Data/BookingRepository.cs +++ b/App/Modules/Bookings/Data/BookingRepository.cs @@ -8,7 +8,7 @@ namespace App.Modules.Bookings.Data; -public class BookingRepository(MainDbContext db, ILogger logger) : ITrainBookingRepository +public class BookingRepository(MainDbContext db, ILogger logger) : IBookingRepository { public async Task>> Search(BookingSearch search) { @@ -53,6 +53,7 @@ public async Task>> Search(BookingSearch se && (userId == null || x.UserId == userId) ) + .Include(x => x.User) .FirstOrDefaultAsync(); return booking?.ToDomain(); } @@ -64,13 +65,13 @@ public async Task>> Search(BookingSearch se } } - public async Task> Create(string? userId, BookingRecord record) + public async Task> Create(string userId, BookingRecord record) { try { logger.LogInformation("Creating Booking: {@Record}", record.ToJson()); - var data = new BookingData(); + var data = new BookingData { UserId = userId }; data = data.UpdateData(record); @@ -164,13 +165,13 @@ public async Task>> Count(DateOnly date, TimeOn { try { - logger.LogInformation("Get booking count"); + logger.LogInformation("Get booking count from {Date} and {Time}...", date, time); var polls = await db.Bookings .Where(x => (x.Date > date || (x.Date == date && x.Time >= time)) && - x.Status != 0 + x.Status == (int)BookStatus.Pending ) .GroupBy(x => new { x.Date, x.Time }) .Select(group => diff --git a/App/Modules/Common/BaseController.cs b/App/Modules/Common/BaseController.cs index 80af744..027e0d5 100644 --- a/App/Modules/Common/BaseController.cs +++ b/App/Modules/Common/BaseController.cs @@ -10,7 +10,7 @@ namespace App.Modules.Common; -public class AtomiControllerBase(AuthHelper h) : ControllerBase +public class AtomiControllerBase(IAuthHelper h) : ControllerBase { protected ActionResult Error(HttpStatusCode code, IDomainProblem problem) { @@ -105,6 +105,7 @@ protected Result GuardOrAll(string? target, string field, params string[] || h.HasAll(this.HttpContext.User, field, value) ) return new Unit().ToResult(); + h.Logger.LogInformation("Auth Failed (All): Target: {Target}, Sub: {Sub}, Field: {Field}, Value: {@Value}, Target Pass: {TargetPass}, Field Pass: {FieldPass}", target, this.Sub(), field, value, target != null && this.Sub() == target, h.HasAny(this.HttpContext.User, field, value)); return new Unauthorized("You are not authorized to access this resource").ToException(); } @@ -115,11 +116,14 @@ protected Task> GuardOrAllAsync(string? target, string field, param protected Result GuardOrAny(string? target, string field, params string[] value) { + if ( (target != null && this.Sub() == target) || h.HasAny(this.HttpContext.User, field, value) ) return new Unit().ToResult(); + + h.Logger.LogInformation("Auth Failed (Any): Target: {Target}, Sub: {Sub}, Field: {Field}, Value: {@Value}, Target Pass: {TargetPass}, Field Pass: {FieldPass}", target, this.Sub(), field, value, target != null && this.Sub() == target, h.HasAny(this.HttpContext.User, field, value)); return new Unauthorized("You are not authorized to access this resource").ToException(); } diff --git a/App/Modules/DomainServices.cs b/App/Modules/DomainServices.cs index 7444849..20ac026 100644 --- a/App/Modules/DomainServices.cs +++ b/App/Modules/DomainServices.cs @@ -1,8 +1,10 @@ +using App.Modules.Bookings.Data; using App.Modules.Passengers.Data; using App.Modules.Schedules.Data; using App.Modules.Timings.Data; using App.Modules.Users.Data; using App.StartUp.Services; +using Domain.Booking; using Domain.Passenger; using Domain.Schedule; using Domain.Timings; @@ -42,6 +44,15 @@ public static IServiceCollection AddDomainServices(this IServiceCollection s) s.AddScoped() .AutoTrace(); + // Bookings + s.AddScoped() + .AutoTrace(); + + s.AddScoped() + .AutoTrace(); + + + return s; } } diff --git a/App/Modules/Passengers/API/V1/PassengerController.cs b/App/Modules/Passengers/API/V1/PassengerController.cs index 0ff152e..8963aab 100644 --- a/App/Modules/Passengers/API/V1/PassengerController.cs +++ b/App/Modules/Passengers/API/V1/PassengerController.cs @@ -21,12 +21,14 @@ public class PassengerController( CreatePassengerReqValidator createPassengerReqValidator, UpdatePassengerReqValidator updatePassengerReqValidator, PassengerSearchQueryValidator passengerSearchQueryValidator, - AuthHelper authHelper + ILogger logger, + IAuthHelper authHelper ) : AtomiControllerBase(authHelper) { [Authorize, HttpGet] public async Task>> Search([FromQuery] SearchPassengerQuery query) { + logger.LogInformation("Searching for passengers, query: {@Query}", query); var x = await this .GuardOrAnyAsync(query.UserId, AuthRoles.Field, AuthRoles.Admin) .ThenAwait(_ => passengerSearchQueryValidator.ValidateAsyncResult(query, "Invalid SearchPassengerQuery")) diff --git a/App/Modules/Passengers/Data/PassengerRepository.cs b/App/Modules/Passengers/Data/PassengerRepository.cs index 028a1f2..eadc371 100644 --- a/App/Modules/Passengers/Data/PassengerRepository.cs +++ b/App/Modules/Passengers/Data/PassengerRepository.cs @@ -50,6 +50,7 @@ public async Task>> Search(PassengerSearc var user = await db .Passengers .Where(x => x.Id == id && (userId == null || x.UserId == userId)) + .Include(x => x.User) .FirstOrDefaultAsync(); return user?.ToDomain(); } diff --git a/App/Modules/Schedules/API/V1/ScheduleController.cs b/App/Modules/Schedules/API/V1/ScheduleController.cs index 79e8341..a1c6dd6 100644 --- a/App/Modules/Schedules/API/V1/ScheduleController.cs +++ b/App/Modules/Schedules/API/V1/ScheduleController.cs @@ -23,7 +23,7 @@ public class ScheduleController( ScheduleBulkUpdateReqValidator schedulePrincipalReqValidator, ScheduleRangeReqValidator scheduleRangeReqValidator, ScheduleDateReqValidator scheduleDateReqValidator, - AuthHelper authHelper + IAuthHelper authHelper ) : AtomiControllerBase(authHelper) { [Authorize(Policy = AuthPolicies.AdminOrScheduleSyncer), HttpGet("latest")] @@ -56,13 +56,13 @@ public async Task> Get([FromRoute] ScheduleDa } [Authorize(Policy = AuthPolicies.AdminOrScheduleSyncer), HttpPut("{date}")] - public async Task> Update([FromRoute] ScheduleDateReq date, + public async Task> Update([FromRoute] ScheduleDateReq dateReq, [FromBody] ScheduleRecordReq record) { var result = await scheduleDateReqValidator - .ValidateAsyncResult(date, "Invalid ScheduleGetReq") + .ValidateAsyncResult(dateReq, "Invalid ScheduleGetReq") .ThenAwait(_ => scheduleRecordReqValidator.ValidateAsyncResult(record, "Invalid ScheduleRecordReq")) - .ThenAwait(_ => service.Update(date.Date.ToDate(), record.ToDomain())) + .ThenAwait(_ => service.Update(dateReq.Date.ToDate(), record.ToDomain())) .Then(x => x.ToRes(), Errors.MapAll); return this.ReturnResult(result); } @@ -77,11 +77,11 @@ public async Task BulkUpdate([FromBody] ScheduleBulkUpdateReq sche } [Authorize(Policy = AuthPolicies.OnlyAdmin), HttpDelete("{date}")] - public async Task Delete([FromRoute] ScheduleDateReq date) + public async Task Delete([FromRoute] ScheduleDateReq req) { var result = await scheduleDateReqValidator - .ValidateAsyncResult(date, "Invalid ScheduleGetReq") + .ValidateAsyncResult(req, "Invalid ScheduleGetReq") .ThenAwait(x => service.Delete(x.Date.ToDate())); - return this.ReturnUnitNullableResult(result, new EntityNotFound("Schedule Not Found", typeof(Schedule), date.Date)); + return this.ReturnUnitNullableResult(result, new EntityNotFound("Schedule Not Found", typeof(Schedule), req.Date)); } } diff --git a/App/Modules/Schedules/API/V1/ScheduleMapper.cs b/App/Modules/Schedules/API/V1/ScheduleMapper.cs index e55c6fa..5622919 100644 --- a/App/Modules/Schedules/API/V1/ScheduleMapper.cs +++ b/App/Modules/Schedules/API/V1/ScheduleMapper.cs @@ -31,5 +31,9 @@ public static ScheduleRecord ToDomain(this ScheduleRecordReq record) => public static SchedulePrincipal ToDomain(this SchedulePrincipalReq principal) => - new() { Date = principal.Date.ToDate(), Record = principal.Record.ToDomain(), }; + new() + { + Date = principal.Date.ToDate(), + Record = principal.Record.ToDomain(), + }; } diff --git a/App/Modules/Schedules/API/V1/ScheduleModel.cs b/App/Modules/Schedules/API/V1/ScheduleModel.cs index 932bc66..e30d86c 100644 --- a/App/Modules/Schedules/API/V1/ScheduleModel.cs +++ b/App/Modules/Schedules/API/V1/ScheduleModel.cs @@ -1,11 +1,12 @@ using App.Modules.Users.API.V1; +using Microsoft.AspNetCore.Mvc; namespace App.Modules.Schedules.API.V1; // REQ -public record ScheduleRangeReq(string From, string To); +public record ScheduleRangeReq([FromRoute] string From, [FromRoute] string To); -public record ScheduleDateReq(string Date); +public record ScheduleDateReq([FromRoute] string Date); public record ScheduleRecordReq(bool Confirmed, string[] JToWExcluded, string[] WToJExcluded); diff --git a/App/Modules/Schedules/Data/ScheduleMapper.cs b/App/Modules/Schedules/Data/ScheduleMapper.cs index 330aa75..a406a74 100644 --- a/App/Modules/Schedules/Data/ScheduleMapper.cs +++ b/App/Modules/Schedules/Data/ScheduleMapper.cs @@ -36,6 +36,7 @@ public static ScheduleData ToData(this SchedulePrincipal record) return new ScheduleData { Date = record.Date, + Confirmed = record.Record.Confirmed, JToWExcluded = record.Record.JToWExcluded.ToArray(), WToJExcluded = record.Record.WToJExcluded.ToArray(), }; diff --git a/App/Modules/Schedules/Data/ScheduleRepository.cs b/App/Modules/Schedules/Data/ScheduleRepository.cs index be6a959..e1c1ac7 100644 --- a/App/Modules/Schedules/Data/ScheduleRepository.cs +++ b/App/Modules/Schedules/Data/ScheduleRepository.cs @@ -1,9 +1,7 @@ using App.Error.V1; -using App.Modules.Passengers.Data; using App.StartUp.Database; using App.Utility; using CSharp_Result; -using Domain.Passenger; using Domain.Schedule; using EFCore.BulkExtensions; using EntityFramework.Exceptions.Common; @@ -114,7 +112,7 @@ public async Task> BulkUpdate(IEnumerable record logger.LogInformation("Bulk updating Schedule, {@Records}", record.ToJson()); await db.BulkInsertOrUpdateAsync(record.Select(r => r.ToData())); await db.BulkSaveChangesAsync(); - return new(); + return new Unit(); } catch (Exception e) { @@ -132,11 +130,15 @@ public async Task> BulkUpdate(IEnumerable record .Where(x => x.Date == date) .FirstOrDefaultAsync(); - if (v1 == null) return (Unit?)null; + if (v1 == null) + { + logger.LogInformation("Schedule on '{@Date}' does not exist.", date); + return (Unit?)null; + } db.Schedules.Remove(v1); await db.SaveChangesAsync(); - return new(); + return new Unit(); } catch (Exception e) { diff --git a/App/Modules/System/SystemController.cs b/App/Modules/System/SystemController.cs index 54b7787..67e2c56 100644 --- a/App/Modules/System/SystemController.cs +++ b/App/Modules/System/SystemController.cs @@ -10,7 +10,7 @@ namespace App.Modules.System; [ApiVersionNeutral] [ApiController] [Route("/")] -public class SystemController(IOptionsSnapshot app, AuthHelper h) : AtomiControllerBase(h) +public class SystemController(IOptionsSnapshot app, IAuthHelper h) : AtomiControllerBase(h) { [HttpGet] public ActionResult SystemInfo() diff --git a/App/Modules/System/V1ErrorController.cs b/App/Modules/System/V1ErrorController.cs index 7779a34..7f86a91 100644 --- a/App/Modules/System/V1ErrorController.cs +++ b/App/Modules/System/V1ErrorController.cs @@ -14,7 +14,7 @@ namespace App.Modules.System; [ApiController] [ApiVersion(1.0)] [Route("api/v{version:apiVersion}/error-info")] -public class V1ErrorController(AuthHelper h) : AtomiControllerBase(h) +public class V1ErrorController(IAuthHelper h) : AtomiControllerBase(h) { private static readonly IEnumerable V1ProblemTypes = from t in Assembly.GetExecutingAssembly().GetTypes() diff --git a/App/Modules/Timings/API/V1/TimingController.cs b/App/Modules/Timings/API/V1/TimingController.cs index 8401ddc..eb50a61 100644 --- a/App/Modules/Timings/API/V1/TimingController.cs +++ b/App/Modules/Timings/API/V1/TimingController.cs @@ -6,9 +6,7 @@ using App.Utility; using Asp.Versioning; using CSharp_Result; -using Domain.Schedule; using Domain.Timings; -using Humanizer; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -22,7 +20,7 @@ public class TimingController( ITimingService service, TrainDirectionReqValidator trainDirectionReqValidator, TimingReqValidator timingReqValidator, - AuthHelper authHelper + IAuthHelper authHelper ) : AtomiControllerBase(authHelper) { diff --git a/App/Modules/Timings/Data/TimingMapper.cs b/App/Modules/Timings/Data/TimingMapper.cs index 840a837..b3d7a68 100644 --- a/App/Modules/Timings/Data/TimingMapper.cs +++ b/App/Modules/Timings/Data/TimingMapper.cs @@ -20,8 +20,8 @@ public static class TimingMapper public static TrainDirection ToTrainDirection(this int d) => d switch { - 0 => TrainDirection.JToW, - 1 => TrainDirection.WToJ, + 1 => TrainDirection.JToW, + 2 => TrainDirection.WToJ, _ => throw new ArgumentOutOfRangeException(nameof(d), d, "Invalid direction"), }; @@ -29,8 +29,8 @@ public static class TimingMapper public static int ToData(this TrainDirection direction) => direction switch { - TrainDirection.JToW => 0, - TrainDirection.WToJ => 1, + TrainDirection.JToW => 1, + TrainDirection.WToJ => 2, _ => throw new ArgumentOutOfRangeException(nameof(direction), direction, "Invalid direction"), }; diff --git a/App/Modules/Timings/Data/TimingRepository.cs b/App/Modules/Timings/Data/TimingRepository.cs index 944b1bf..2997229 100644 --- a/App/Modules/Timings/Data/TimingRepository.cs +++ b/App/Modules/Timings/Data/TimingRepository.cs @@ -33,12 +33,16 @@ public class TimingRepository(MainDbContext db, IRedisClientFactory factory, ILo if (ret == null) return (Timing?)null; logger.LogInformation("Caching timings for {@Direction}...", direction); - var success = await this.Redis.AddAsync(redisKey, ret.Timings.Select(x => x.ToStandardTimeFormat())); + + logger.LogInformation("Standard Timings: {@Timings}", ret.Timings); + + var success = await this.Redis.AddAsync(redisKey, ret.Timings.Select(x => x.ToStandardTimeFormat()).ToArray()); if (!success) logger.LogWarning("Failed to cache timings for {@Direction}", direction); else logger.LogInformation("Successfully cached timings for {@Direction}", direction); return ret.ToDomain(); } + logger.LogInformation("Obtained timings for {@Direction} from cache", direction); return new Timing { Principal = new() { Direction = direction, Record = new() { Timings = r.Select(x => x.ToTime()) } } diff --git a/App/Modules/Users/API/V1/UserController.cs b/App/Modules/Users/API/V1/UserController.cs index 5fa7a4f..3f170b9 100644 --- a/App/Modules/Users/API/V1/UserController.cs +++ b/App/Modules/Users/API/V1/UserController.cs @@ -25,7 +25,7 @@ public class UserController( CreateUserReqValidator createUserReqValidator, UpdateUserReqValidator updateUserReqValidator, UserSearchQueryValidator userSearchQueryValidator, - AuthHelper h + IAuthHelper h ) : AtomiControllerBase(h) { [Authorize(Policy = AuthPolicies.OnlyAdmin), HttpGet] diff --git a/App/StartUp/Database/MainDbContext.cs b/App/StartUp/Database/MainDbContext.cs index 7271ca4..aaae0bc 100644 --- a/App/StartUp/Database/MainDbContext.cs +++ b/App/StartUp/Database/MainDbContext.cs @@ -14,7 +14,7 @@ namespace App.StartUp.Database; -public class MainDbContext(IOptionsMonitor> options) +public class MainDbContext(IOptionsMonitor> options, ILoggerFactory factory) : DbContext { public const string Key = "MAIN"; @@ -69,6 +69,7 @@ public class MainDbContext(IOptionsMonitor> o protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder + .UseLoggerFactory(factory) .AddPostgres(options.CurrentValue, Key) .UseExceptionProcessor(); } @@ -86,6 +87,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) booking.Property(x => x.CreatedAt).HasDefaultValueSql("NOW()"); booking.Property(x => x.CompletedAt).HasDefaultValue(null); booking.Property(x => x.Status).HasDefaultValue(0); + booking.OwnsMany(x => x.Passengers, x => x.ToJson()); var timings = modelBuilder.Entity(); diff --git a/App/StartUp/Services/Auth/AuthHelper.cs b/App/StartUp/Services/Auth/AuthHelper.cs index d9f4705..79a8935 100644 --- a/App/StartUp/Services/Auth/AuthHelper.cs +++ b/App/StartUp/Services/Auth/AuthHelper.cs @@ -4,7 +4,16 @@ namespace App.StartUp.Services.Auth; -public class AuthHelper(IOptionsMonitor authOption) +public interface IAuthHelper +{ + bool HasAll(ClaimsPrincipal? user, string field, params string[] scopes); + + bool HasAny(ClaimsPrincipal? user, string field, params string[] scopes); + + ILogger Logger { get; } +} + +public class AuthHelper(IOptionsMonitor authOption, ILogger logger) : IAuthHelper { private string? Issuer => authOption.CurrentValue.Settings?.Issuer; @@ -14,23 +23,29 @@ private IEnumerable FieldToScope(ClaimsPrincipal? user, string field) ? "http://schemas.microsoft.com/ws/2008/06/identity/claims/role" : field; var s = user? - .FindAll(c => c.Type == field && c.Issuer == this.Issuer)? + .FindAll(c => c.Type == f && c.Issuer == this.Issuer)? .Select(x => x.Value); if (field == "scope") s = s?.SelectMany(x => x.Split(' ')); return s ?? []; } + public ILogger Logger => logger; + public bool HasAll(ClaimsPrincipal? user, string field, params string[] scopes) { - var s = this.FieldToScope(user, field); - return scopes.All(scope => s.Contains(scope)); + var r = scopes.All(scope => s.Contains(scope)); + + if (!r) logger.LogInformation("No matching scopes. Field: {RequireField} Needed: {@Require}, Token: {@Token}", field, scopes, s); + return r; } public bool HasAny(ClaimsPrincipal? user, string field, params string[] scopes) { var s = this.FieldToScope(user, field); - return scopes.Any(scope => s.Contains(scope)); + var r = scopes.Any(scope => s.Contains(scope)); + if (!r) logger.LogInformation("No matching scopes. Field: {RequireField} Needed: {@Require}, Token: {@Token}", field, scopes, s); + return r; } } diff --git a/App/StartUp/Services/Auth/HasAllHandler.cs b/App/StartUp/Services/Auth/HasAllHandler.cs index 38e7a79..86bb34b 100644 --- a/App/StartUp/Services/Auth/HasAllHandler.cs +++ b/App/StartUp/Services/Auth/HasAllHandler.cs @@ -4,7 +4,7 @@ namespace App.StartUp.Services.Auth; -public class HasAllHandler : AuthorizationHandler +public class HasAllHandler(ILogger> logger) : AuthorizationHandler { protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, HasAllRequirement requirement) @@ -13,19 +13,26 @@ protected override Task HandleRequirementAsync(AuthorizationHandlerContext conte var field = requirement.Field == "roles" ? "http://schemas.microsoft.com/ws/2008/06/identity/claims/role" : requirement.Field; + var scopes = context.User .FindAll(c => c.Type == field && c.Issuer == requirement.Issuer)? .Select(x => x.Value); + if (requirement.Field == "scope") scopes = scopes?.SelectMany(x => x.Split(' ')); - if (scopes == null) return Task.CompletedTask; + if (scopes == null) + { + logger.LogInformation("No scopes found in the token"); + return Task.CompletedTask; + } // Succeed if the scope array contains the required scope if (requirement.Scope.All(s => scopes.Contains(s))) context.Succeed(requirement); - + else + logger.LogInformation("No matching scopes. Field: {RequireField} Needed: {@Require}, Token: {@Token}", requirement.Field, requirement.Scope, scopes); return Task.CompletedTask; } } diff --git a/App/StartUp/Services/Auth/HasAnyHandler.cs b/App/StartUp/Services/Auth/HasAnyHandler.cs index 53c4bfb..aec6053 100644 --- a/App/StartUp/Services/Auth/HasAnyHandler.cs +++ b/App/StartUp/Services/Auth/HasAnyHandler.cs @@ -1,21 +1,34 @@ +using App.Utility; using Microsoft.AspNetCore.Authorization; namespace App.StartUp.Services.Auth; -public class HasAnyHandler : AuthorizationHandler +public class HasAnyHandler(ILogger> logger) : AuthorizationHandler { protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, HasAnyRequirement requirement) { // Split the scopes string into an array - var scopes = context.User.FindFirst(c => c.Type == "scope" && c.Issuer == requirement.Issuer)?.Value - .Split(' '); + var field = requirement.Field == "roles" + ? "http://schemas.microsoft.com/ws/2008/06/identity/claims/role" + : requirement.Field; - if (scopes == null) return Task.CompletedTask; + var scopes = context.User + .FindAll(c => c.Type == field && c.Issuer == requirement.Issuer)? + .Select(x => x.Value); + + + if (scopes == null) + { + logger.LogInformation("No scopes found in the token"); + return Task.CompletedTask; + } // Succeed if the scope array contains the required scope if (requirement.Scope.Any(s => scopes.Contains(s))) context.Succeed(requirement); + else + logger.LogInformation("No matching scopes. Field: {RequireField} Needed: {@Require}, Token: {@Token}", requirement.Field, requirement.Scope, scopes); return Task.CompletedTask; } diff --git a/App/StartUp/Services/AuthService.cs b/App/StartUp/Services/AuthService.cs index 32debe0..b583d40 100644 --- a/App/StartUp/Services/AuthService.cs +++ b/App/StartUp/Services/AuthService.cs @@ -26,8 +26,8 @@ public static IServiceCollection AddAuthService(this IServiceCollection services services.AddSingleton() .AutoTrace(); - services.AddSingleton() - .AutoTrace(); + services.AddSingleton() + .AutoTrace(); var s = o.Settings!; var domain = $"https://{s.Domain}"; diff --git a/App/Utility/Utils.cs b/App/Utility/Utils.cs index 5f4fb4d..a4d900b 100644 --- a/App/Utility/Utils.cs +++ b/App/Utility/Utils.cs @@ -41,7 +41,7 @@ public static string ToStandardDateFormat(this DateOnly date) => date.ToString(StandardDateFormat); public static string ToStandardTimeFormat(this TimeOnly time) => - time.ToString(StandardDateFormat); + time.ToString(StandardTimeFormat); public static DomainProblemException ToException(this IDomainProblem p) diff --git a/App/packages.lock.json b/App/packages.lock.json index 4ea240f..801e8f1 100644 --- a/App/packages.lock.json +++ b/App/packages.lock.json @@ -1743,7 +1743,8 @@ "domain": { "type": "Project", "dependencies": { - "CSharp-Result": "[0.2.0, )" + "CSharp-Result": "[0.2.0, )", + "Microsoft.Extensions.Logging": "[8.0.0, )" } } } diff --git a/Domain/Booking/IService.cs b/Domain/Booking/IService.cs index 840942a..00238fa 100644 --- a/Domain/Booking/IService.cs +++ b/Domain/Booking/IService.cs @@ -2,13 +2,13 @@ namespace Domain.Booking; -public interface ITrainBookingService +public interface IBookingService { Task>> Search(BookingSearch search); Task> Get(string? userId, Guid id); - Task> Create(string? userId, BookingRecord record); + Task> Create(string userId, BookingRecord record); Task> Update(string? userId, Guid id, BookingRecord record); diff --git a/Domain/Booking/Repository.cs b/Domain/Booking/Repository.cs index 0fee392..5fc9596 100644 --- a/Domain/Booking/Repository.cs +++ b/Domain/Booking/Repository.cs @@ -2,13 +2,13 @@ namespace Domain.Booking; -public interface ITrainBookingRepository +public interface IBookingRepository { Task>> Search(BookingSearch search); Task> Get(string? userId, Guid id); - Task> Create(string? userId, BookingRecord record); + Task> Create(string userId, BookingRecord record); Task> Update(string? userId, Guid id, BookingStatus? status, BookingRecord? record); diff --git a/Domain/Booking/Service.cs b/Domain/Booking/Service.cs index a214ddf..eb690eb 100644 --- a/Domain/Booking/Service.cs +++ b/Domain/Booking/Service.cs @@ -1,8 +1,9 @@ using CSharp_Result; +using Microsoft.Extensions.Logging; namespace Domain.Booking; -public class TrainBookingService(ITrainBookingRepository repo) : ITrainBookingService +public class BookingService(IBookingRepository repo, ILogger logger) : IBookingService { public Task>> Search(BookingSearch search) { @@ -14,7 +15,7 @@ public Task>> Search(BookingSearch search) return repo.Get(userId, id); } - public Task> Create(string? userId, BookingRecord record) + public Task> Create(string userId, BookingRecord record) { return repo.Create(userId, record); } @@ -26,13 +27,13 @@ public Task> Create(string? userId, BookingRecord recor public Task> Complete(Guid id) { - return repo.Update(null, id, new BookingStatus { Status = BookStatus.Completed, CompletedAt = DateTime.Now, }, + return repo.Update(null, id, new BookingStatus { Status = BookStatus.Completed, CompletedAt = DateTime.UtcNow, }, null); } public Task> Cancel(Guid id) { - return repo.Update(null, id, new BookingStatus { Status = BookStatus.Cancelled, CompletedAt = DateTime.Now, }, + return repo.Update(null, id, new BookingStatus { Status = BookStatus.Cancelled, CompletedAt = DateTime.UtcNow, }, null); } @@ -45,10 +46,12 @@ public Task> Create(string? userId, BookingRecord recor public Task>> Count() { var singapore = TimeZoneInfo.FindSystemTimeZoneById("Singapore"); - var now = TimeZoneInfo.ConvertTimeFromUtc(DateTime.Now, singapore); + var now = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, singapore); var dateNow = DateOnly.FromDateTime(now); var timeNow = TimeOnly.FromDateTime(now); + logger.LogInformation("Get booking count after {Date} {Time}", dateNow, timeNow); + return repo.Count(dateNow, timeNow); } } diff --git a/Domain/Domain.csproj b/Domain/Domain.csproj index 3f4c106..dcbad73 100644 --- a/Domain/Domain.csproj +++ b/Domain/Domain.csproj @@ -8,6 +8,7 @@ + diff --git a/Domain/packages.lock.json b/Domain/packages.lock.json index 92962b3..8c594f3 100644 --- a/Domain/packages.lock.json +++ b/Domain/packages.lock.json @@ -7,6 +7,52 @@ "requested": "[0.2.0, )", "resolved": "0.2.0", "contentHash": "VkmXrucUsU2nfzhy2Y44ufmc8jkiZ0AM+0LVTZBV2HmbzLRDk2oFfS34SfxTrUyy027zs1ipEqVINe1mE9PX2g==" + }, + "Microsoft.Extensions.Logging": { + "type": "Direct", + "requested": "[8.0.0, )", + "resolved": "8.0.0", + "contentHash": "tvRkov9tAJ3xP51LCv3FJ2zINmv1P8Hi8lhhtcKGqM+ImiTCC84uOPEI4z8Cdq2C3o9e+Aa0Gw0rmrsJD77W+w==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "8.0.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "V8S3bsm50ig6JSyrbcJJ8bW2b9QLGouz+G1miK3UTaOWmMtFwNNNzUf4AleyDWUmTrWMLNnFSLEQtxmxgNQnNQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "cjWrLkJXK0rs4zofsK4bSdg+jhDLTaxrkXu4gS6Y7MAlCvRyNNgwY/lJi5RDlQOnSZweHqoyvgvbdvQsRIW+hg==" + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" + } + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "JOVOfqpnqlVLUzINQ2fox8evY2SKLYJ3BV8QDe/Jyp21u1T7r45x/R/5QdteURMR5r01GxeJSBBUOCOyaNXA3g==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g==" } } } diff --git a/infra/api_chart/app/settings.example.yaml b/infra/api_chart/app/settings.example.yaml index a7999d1..f55e85e 100644 --- a/infra/api_chart/app/settings.example.yaml +++ b/infra/api_chart/app/settings.example.yaml @@ -10,12 +10,6 @@ Logging: Console: LogLevel: Default: Information - FormatterName: json - FormatterOptions: - SingleLine: false - IncludeScopes: true - JsonWriterOptions: - Indented: true OpenTelemetry: IncludeFormattedMessage: true IncludeScopes: true diff --git a/infra/api_chart/app/settings.yaml b/infra/api_chart/app/settings.yaml index c2b9467..259222b 100644 --- a/infra/api_chart/app/settings.yaml +++ b/infra/api_chart/app/settings.yaml @@ -7,14 +7,14 @@ Kestrel: Url: http://+:9001 Logging: LogLevel: - Default: None - Console: - LogLevel: - Default: Information + Default: Information + # Console: + # LogLevel: + # Default: Debug OpenTelemetry: - IncludeFormattedMessage: true - IncludeScopes: true - ParseStateValues: true + IncludeFormattedMessage: false + IncludeScopes: false + ParseStateValues: false # Domain App: Landscape: lapras diff --git a/infra/migration_chart/app/settings.example.yaml b/infra/migration_chart/app/settings.example.yaml index a7999d1..f55e85e 100644 --- a/infra/migration_chart/app/settings.example.yaml +++ b/infra/migration_chart/app/settings.example.yaml @@ -10,12 +10,6 @@ Logging: Console: LogLevel: Default: Information - FormatterName: json - FormatterOptions: - SingleLine: false - IncludeScopes: true - JsonWriterOptions: - Indented: true OpenTelemetry: IncludeFormattedMessage: true IncludeScopes: true diff --git a/infra/migration_chart/app/settings.yaml b/infra/migration_chart/app/settings.yaml index c2b9467..259222b 100644 --- a/infra/migration_chart/app/settings.yaml +++ b/infra/migration_chart/app/settings.yaml @@ -7,14 +7,14 @@ Kestrel: Url: http://+:9001 Logging: LogLevel: - Default: None - Console: - LogLevel: - Default: Information + Default: Information + # Console: + # LogLevel: + # Default: Debug OpenTelemetry: - IncludeFormattedMessage: true - IncludeScopes: true - ParseStateValues: true + IncludeFormattedMessage: false + IncludeScopes: false + ParseStateValues: false # Domain App: Landscape: lapras