From 4353a203f5280537f308ad1bea4dc6377b3ab6d3 Mon Sep 17 00:00:00 2001 From: Igor Gomes Date: Tue, 3 Mar 2026 11:40:59 -0300 Subject: [PATCH 1/3] 02 - Create test to check race condition control --- .../Reservation/CreateReservationTest.cs | 75 ++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/FC4.HotelReservation/modular monolith/tests/FC4.HotelReservation.IntegrationTests/Reservation/CreateReservationTest.cs b/FC4.HotelReservation/modular monolith/tests/FC4.HotelReservation.IntegrationTests/Reservation/CreateReservationTest.cs index 7e0d553..d60d4ab 100644 --- a/FC4.HotelReservation/modular monolith/tests/FC4.HotelReservation.IntegrationTests/Reservation/CreateReservationTest.cs +++ b/FC4.HotelReservation/modular monolith/tests/FC4.HotelReservation.IntegrationTests/Reservation/CreateReservationTest.cs @@ -96,7 +96,7 @@ await fixture.CreateRoomTypeRateInDatabaseAsync( payment.Amount.Value.Should().Be(expectedTotalAmount); payment.ProcessedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromMinutes(1)); } - + [Fact] public async Task CreateReservation_WithInsufficientInventory_ShouldReturnBadRequest() { @@ -141,6 +141,79 @@ await fixture.CreateRoomTypeInventoryInDatabaseAsync( inventory.Should().NotBeNull(); inventory.TotalReserved.Should().Be(0); } + + [Fact] + public async Task CreateReservation_WithConcurrentRequests_OnlyOneShouldSucceed() + { + // Arrange + var hotel = await fixture.CreateHotelInDatabaseAsync(); + var guest = await fixture.CreateGuestInDatabaseAsync(); + var roomType = await fixture.CreateRoomTypeInDatabaseAsync(); + var startDate = DateTime.Today.AddDays(10); + var endDate = startDate.AddDays(2); + const int totalInventory = 1; + const int roomQuantity = 1; + const int concurrentRequests = 4; + + var dates = new DateRange(startDate, endDate).GetDates().ToList(); + foreach (var date in dates) + { + await fixture.CreateRoomTypeInventoryInDatabaseAsync( + ARoomTypeInventory() + .WithHotelId(hotel.Id) + .WithRoomTypeId(roomType.Id) + .WithDate(date) + .WithTotalInventory(totalInventory) + .Build()); + + await fixture.CreateRoomTypeRateInDatabaseAsync( + ARoomTypeRate() + .WithHotelId(hotel.Id) + .WithRoomTypeId(roomType.Id) + .WithDate(date) + .WithRate(new Catalog.Domain.ValueObjects.Money(100m, "USD")) + .Build()); + } + + var input = ACreateReservationInput() + .WithHotelId(hotel.Id) + .WithRoomTypeId(roomType.Id) + .WithGuestId(guest.Id) + .WithStartDate(startDate) + .WithEndDate(endDate) + .WithRoomQuantity(roomQuantity) + .Build(); + + var barrier = new Barrier(concurrentRequests); + var tasks = Enumerable.Range(0, concurrentRequests).Select(_ => Task.Run(async () => + { + using var client = fixture.CreateClient(); + barrier.SignalAndWait(); + return await client.PostAsJsonAsync("/v1/reservations", input); + })).ToList(); + + var responses = await Task.WhenAll(tasks); + + // Assert – exactly one request succeeded + var successResponses = responses.Where(r => r.StatusCode == HttpStatusCode.Created).ToList(); + var failureResponses = responses.Where(r => r.StatusCode != HttpStatusCode.Created).ToList(); + + successResponses.Should().HaveCount(1, "only one reservation should succeed when there is a single room available"); + failureResponses.Should().HaveCount(concurrentRequests - 1); + failureResponses.Should().OnlyContain(r => + r.StatusCode == HttpStatusCode.UnprocessableEntity || + r.StatusCode == HttpStatusCode.Conflict); + + // Verify only one reservation exists + var reservations = await fixture.GetReservationsByGuestIdAsync(guest.Id); + reservations.Should().HaveCount(1); + + // Verify inventory reflects exactly one reservation + var period = new DateRange(startDate, endDate); + var updatedInventories = await fixture.GetRoomTypeInventoriesAsync(hotel.Id, roomType.Id, period); + updatedInventories.Should().OnlyContain(i => i.TotalReserved == roomQuantity); + updatedInventories.Should().OnlyContain(i => i.AvailableInventory == totalInventory - roomQuantity); + } public async ValueTask DisposeAsync() { From f1322076e73f29b392d2e56981b09a104e1eae00 Mon Sep 17 00:00:00 2001 From: Igor Gomes Date: Tue, 3 Mar 2026 14:27:09 -0300 Subject: [PATCH 2/3] 03 - Pessimistic lock --- .../CreateReservation/CreateReservation.cs | 3 ++- .../Repositories/RoomTypeInventoryRepository.cs | 14 ++++++++++---- .../UnitOfWork.cs | 5 +++++ 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/FC4.HotelReservation/modular monolith/modules/Reservations/FC4.HotelReservation.Reservations.Application/UseCases/Reservation/CreateReservation/CreateReservation.cs b/FC4.HotelReservation/modular monolith/modules/Reservations/FC4.HotelReservation.Reservations.Application/UseCases/Reservation/CreateReservation/CreateReservation.cs index b54b260..a5bdd84 100644 --- a/FC4.HotelReservation/modular monolith/modules/Reservations/FC4.HotelReservation.Reservations.Application/UseCases/Reservation/CreateReservation/CreateReservation.cs +++ b/FC4.HotelReservation/modular monolith/modules/Reservations/FC4.HotelReservation.Reservations.Application/UseCases/Reservation/CreateReservation/CreateReservation.cs @@ -21,9 +21,10 @@ public async Task Handle( ?? throw new InvalidOperationException("Guest not found"); var period = new DateRange(request.StartDate, request.EndDate); + // Lock pessimista: bloquear os registros de disponibilidade até que a transação seja comitada var inventories = await roomTypeInventoryRepository.GetInventoryForPeriodAsync( request.HotelId, request.RoomTypeId, period, cancellationToken); - + if (!HasSufficientInventory(inventories, period, request.RoomQuantity)) { throw new InvalidOperationException("Not enough rooms available for the requested period"); diff --git a/FC4.HotelReservation/modular monolith/modules/Reservations/FC4.HotelReservation.Reservations.Infra.Data/Repositories/RoomTypeInventoryRepository.cs b/FC4.HotelReservation/modular monolith/modules/Reservations/FC4.HotelReservation.Reservations.Infra.Data/Repositories/RoomTypeInventoryRepository.cs index 85123da..b79ca2c 100644 --- a/FC4.HotelReservation/modular monolith/modules/Reservations/FC4.HotelReservation.Reservations.Infra.Data/Repositories/RoomTypeInventoryRepository.cs +++ b/FC4.HotelReservation/modular monolith/modules/Reservations/FC4.HotelReservation.Reservations.Infra.Data/Repositories/RoomTypeInventoryRepository.cs @@ -14,12 +14,18 @@ public async Task> GetInventoryForPeriodAsync( DateRange period, CancellationToken cancellationToken) { - var dates = period.GetDates().ToList(); + var dates = period.GetDates().Select(d => d.ToString("yyyy-MM-dd")).ToList(); + var datesLiteral = string.Join(", ", dates.Select(d => $"'{d}'::date")); + + var sql = $@"SELECT * FROM room_type_inventory + WHERE hotel_id = {{0}} + AND room_type_id = {{1}} + AND date IN ({datesLiteral}) + FOR UPDATE"; return await context.RoomTypeInventories - .Where(i => i.HotelId == hotelId && - i.RoomTypeId == roomTypeId && - dates.Contains(i.Date)) + .FromSqlRaw(sql, hotelId, roomTypeId) + .AsTracking() .ToListAsync(cancellationToken); } diff --git a/FC4.HotelReservation/modular monolith/shared/FC4.HotelReservation.Shared.Infrastructure/UnitOfWork.cs b/FC4.HotelReservation/modular monolith/shared/FC4.HotelReservation.Shared.Infrastructure/UnitOfWork.cs index 8773c5e..8e78fbf 100644 --- a/FC4.HotelReservation/modular monolith/shared/FC4.HotelReservation.Shared.Infrastructure/UnitOfWork.cs +++ b/FC4.HotelReservation/modular monolith/shared/FC4.HotelReservation.Shared.Infrastructure/UnitOfWork.cs @@ -1,6 +1,7 @@ using FC4.HotelReservation.Shared.Application; using FC4.HotelReservation.Shared.Domain; using MediatR; +using Microsoft.EntityFrameworkCore.Storage; namespace FC4.HotelReservation.Shared.Infrastructure; @@ -8,6 +9,8 @@ public class UnitOfWork( HotelDbContext dbContext, IPublisher publisher) : IUnitOfWork { + private readonly IDbContextTransaction _transaction = dbContext.Database.BeginTransaction(); + public async Task CommitAsync(CancellationToken cancellationToken) { var aggregateRoots = dbContext @@ -27,5 +30,7 @@ public async Task CommitAsync(CancellationToken cancellationToken) } await dbContext.SaveChangesAsync(cancellationToken); + await _transaction.CommitAsync(cancellationToken); + await _transaction.DisposeAsync(); } } \ No newline at end of file From 4eed9ef0b90aad0cd7fc9a511dcac0b8b562e41f Mon Sep 17 00:00:00 2001 From: Igor Gomes Date: Mon, 9 Mar 2026 14:27:08 -0300 Subject: [PATCH 3/3] 05 - implement optimistic lock --- .../Entities/RoomTypeInventory.cs | 3 +- .../RoomTypeInventoryRepository.cs | 3 +- .../Exceptions/ConflictException.cs | 3 + .../IVersioned.cs | 6 + .../RoomTypeInventoryConfiguration.cs | 4 + .../20260309161251_RowVersion.Designer.cs | 530 ++++++++++++++++++ .../Migrations/20260309161251_RowVersion.cs | 29 + .../Migrations/HotelDbContextModelSnapshot.cs | 427 +++++++------- .../UnitOfWork.cs | 31 +- .../GlobalExceptionHandler.cs | 5 + 10 files changed, 822 insertions(+), 219 deletions(-) create mode 100644 FC4.HotelReservation/modular monolith/shared/FC4.HotelReservation.Shared.Application/Exceptions/ConflictException.cs create mode 100644 FC4.HotelReservation/modular monolith/shared/FC4.HotelReservation.Shared.Domain/IVersioned.cs create mode 100644 FC4.HotelReservation/modular monolith/shared/FC4.HotelReservation.Shared.Infrastructure/Migrations/20260309161251_RowVersion.Designer.cs create mode 100644 FC4.HotelReservation/modular monolith/shared/FC4.HotelReservation.Shared.Infrastructure/Migrations/20260309161251_RowVersion.cs diff --git a/FC4.HotelReservation/modular monolith/modules/Reservations/FC4.HotelReservation.Reservations.Domain/Entities/RoomTypeInventory.cs b/FC4.HotelReservation/modular monolith/modules/Reservations/FC4.HotelReservation.Reservations.Domain/Entities/RoomTypeInventory.cs index e92de6d..de82b4a 100644 --- a/FC4.HotelReservation/modular monolith/modules/Reservations/FC4.HotelReservation.Reservations.Domain/Entities/RoomTypeInventory.cs +++ b/FC4.HotelReservation/modular monolith/modules/Reservations/FC4.HotelReservation.Reservations.Domain/Entities/RoomTypeInventory.cs @@ -3,13 +3,14 @@ namespace FC4.HotelReservation.Reservations.Domain.Entities; -public class RoomTypeInventory : AggregateRoot +public class RoomTypeInventory : AggregateRoot, IVersioned { public Guid HotelId { get; private set; } public Guid RoomTypeId { get; private set; } public DateTime Date { get; private set; } public int TotalInventory { get; private set; } public int TotalReserved { get; private set; } + public int Version { get; private set; } private RoomTypeInventory() { diff --git a/FC4.HotelReservation/modular monolith/modules/Reservations/FC4.HotelReservation.Reservations.Infra.Data/Repositories/RoomTypeInventoryRepository.cs b/FC4.HotelReservation/modular monolith/modules/Reservations/FC4.HotelReservation.Reservations.Infra.Data/Repositories/RoomTypeInventoryRepository.cs index b79ca2c..604a71e 100644 --- a/FC4.HotelReservation/modular monolith/modules/Reservations/FC4.HotelReservation.Reservations.Infra.Data/Repositories/RoomTypeInventoryRepository.cs +++ b/FC4.HotelReservation/modular monolith/modules/Reservations/FC4.HotelReservation.Reservations.Infra.Data/Repositories/RoomTypeInventoryRepository.cs @@ -20,8 +20,7 @@ public async Task> GetInventoryForPeriodAsync( var sql = $@"SELECT * FROM room_type_inventory WHERE hotel_id = {{0}} AND room_type_id = {{1}} - AND date IN ({datesLiteral}) - FOR UPDATE"; + AND date IN ({datesLiteral})"; return await context.RoomTypeInventories .FromSqlRaw(sql, hotelId, roomTypeId) diff --git a/FC4.HotelReservation/modular monolith/shared/FC4.HotelReservation.Shared.Application/Exceptions/ConflictException.cs b/FC4.HotelReservation/modular monolith/shared/FC4.HotelReservation.Shared.Application/Exceptions/ConflictException.cs new file mode 100644 index 0000000..8d74104 --- /dev/null +++ b/FC4.HotelReservation/modular monolith/shared/FC4.HotelReservation.Shared.Application/Exceptions/ConflictException.cs @@ -0,0 +1,3 @@ +namespace FC4.HotelReservation.Shared.Application.Exceptions; + +public class ConflictException(string message, Exception innerException) : Exception(message, innerException); \ No newline at end of file diff --git a/FC4.HotelReservation/modular monolith/shared/FC4.HotelReservation.Shared.Domain/IVersioned.cs b/FC4.HotelReservation/modular monolith/shared/FC4.HotelReservation.Shared.Domain/IVersioned.cs new file mode 100644 index 0000000..d9965f2 --- /dev/null +++ b/FC4.HotelReservation/modular monolith/shared/FC4.HotelReservation.Shared.Domain/IVersioned.cs @@ -0,0 +1,6 @@ +namespace FC4.HotelReservation.Shared.Domain; + +public interface IVersioned +{ + int Version { get; } +} \ No newline at end of file diff --git a/FC4.HotelReservation/modular monolith/shared/FC4.HotelReservation.Shared.Infrastructure/Mappings/RoomTypeInventoryConfiguration.cs b/FC4.HotelReservation/modular monolith/shared/FC4.HotelReservation.Shared.Infrastructure/Mappings/RoomTypeInventoryConfiguration.cs index 77fdfc4..a31faf0 100644 --- a/FC4.HotelReservation/modular monolith/shared/FC4.HotelReservation.Shared.Infrastructure/Mappings/RoomTypeInventoryConfiguration.cs +++ b/FC4.HotelReservation/modular monolith/shared/FC4.HotelReservation.Shared.Infrastructure/Mappings/RoomTypeInventoryConfiguration.cs @@ -37,6 +37,10 @@ public void Configure(EntityTypeBuilder builder) builder.Property(rti => rti.TotalReserved) .HasColumnName("total_reserved") .IsRequired(); + + builder.Property(rti => rti.Version) + .HasColumnName("version") + .IsConcurrencyToken(); builder.HasOne() .WithMany() diff --git a/FC4.HotelReservation/modular monolith/shared/FC4.HotelReservation.Shared.Infrastructure/Migrations/20260309161251_RowVersion.Designer.cs b/FC4.HotelReservation/modular monolith/shared/FC4.HotelReservation.Shared.Infrastructure/Migrations/20260309161251_RowVersion.Designer.cs new file mode 100644 index 0000000..78183c7 --- /dev/null +++ b/FC4.HotelReservation/modular monolith/shared/FC4.HotelReservation.Shared.Infrastructure/Migrations/20260309161251_RowVersion.Designer.cs @@ -0,0 +1,530 @@ +// +using System; +using FC4.HotelReservation.Shared.Infrastructure; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace FC4.HotelReservation.Shared.Infrastructure.Migrations +{ + [DbContext(typeof(HotelDbContext))] + [Migration("20260309161251_RowVersion")] + partial class RowVersion + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.19") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("FC4.HotelReservation.Catalog.Domain.Entities.Hotel", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id"); + + b.ToTable("hotels", (string)null); + }); + + modelBuilder.Entity("FC4.HotelReservation.Catalog.Domain.Entities.Room", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Floor") + .HasColumnType("integer") + .HasColumnName("floor"); + + b.Property("HotelId") + .HasColumnType("uuid") + .HasColumnName("hotel_id"); + + b.Property("IsAvailable") + .HasColumnType("boolean") + .HasColumnName("is_available"); + + b.Property("Number") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("number"); + + b.Property("RoomTypeId") + .HasColumnType("uuid") + .HasColumnName("room_type_id"); + + b.HasKey("Id"); + + b.HasIndex("HotelId"); + + b.HasIndex("RoomTypeId"); + + b.ToTable("rooms", (string)null); + }); + + modelBuilder.Entity("FC4.HotelReservation.Catalog.Domain.Entities.RoomType", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("description"); + + b.HasKey("Id"); + + b.ToTable("room_types", (string)null); + }); + + modelBuilder.Entity("FC4.HotelReservation.Catalog.Domain.Entities.RoomTypeRate", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Date") + .HasColumnType("timestamp with time zone") + .HasColumnName("date"); + + b.Property("HotelId") + .HasColumnType("uuid") + .HasColumnName("hotel_id"); + + b.Property("RoomTypeId") + .HasColumnType("uuid") + .HasColumnName("room_type_id"); + + b.HasKey("Id"); + + b.HasIndex("RoomTypeId"); + + b.HasIndex("HotelId", "RoomTypeId", "Date") + .IsUnique(); + + b.ToTable("room_type_rates", (string)null); + }); + + modelBuilder.Entity("FC4.HotelReservation.Guests.Domain.Entities.Guest", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("first_name"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("last_name"); + + b.HasKey("Id"); + + b.ToTable("guests", (string)null); + }); + + modelBuilder.Entity("FC4.HotelReservation.Payments.Domain.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ProcessedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("processed_at"); + + b.Property("ReservationId") + .HasColumnType("uuid") + .HasColumnName("reservation_id"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text") + .HasColumnName("status"); + + b.Property("TransactionId") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("transaction_id"); + + b.HasKey("Id"); + + b.HasIndex("ReservationId"); + + b.ToTable("payments", (string)null); + }); + + modelBuilder.Entity("FC4.HotelReservation.Reservations.Domain.Entities.Reservation", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("GuestId") + .HasColumnType("uuid") + .HasColumnName("guest_id"); + + b.Property("HotelId") + .HasColumnType("uuid") + .HasColumnName("hotel_id"); + + b.Property("RoomQuantity") + .HasColumnType("integer") + .HasColumnName("room_quantity"); + + b.Property("RoomTypeId") + .HasColumnType("uuid") + .HasColumnName("room_type_id"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text") + .HasColumnName("status"); + + b.HasKey("Id"); + + b.HasIndex("GuestId"); + + b.HasIndex("HotelId"); + + b.HasIndex("RoomTypeId"); + + b.ToTable("reservations", (string)null); + }); + + modelBuilder.Entity("FC4.HotelReservation.Reservations.Domain.Entities.RoomTypeInventory", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Date") + .HasColumnType("timestamp with time zone") + .HasColumnName("date"); + + b.Property("HotelId") + .HasColumnType("uuid") + .HasColumnName("hotel_id"); + + b.Property("RoomTypeId") + .HasColumnType("uuid") + .HasColumnName("room_type_id"); + + b.Property("TotalInventory") + .HasColumnType("integer") + .HasColumnName("total_inventory"); + + b.Property("TotalReserved") + .HasColumnType("integer") + .HasColumnName("total_reserved"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("integer") + .HasColumnName("version"); + + b.HasKey("Id"); + + b.HasIndex("RoomTypeId"); + + b.HasIndex("HotelId", "RoomTypeId", "Date") + .IsUnique(); + + b.ToTable("room_type_inventory", (string)null); + }); + + modelBuilder.Entity("FC4.HotelReservation.Catalog.Domain.Entities.Hotel", b => + { + b.OwnsOne("FC4.HotelReservation.Catalog.Domain.ValueObjects.Address", "Address", b1 => + { + b1.Property("HotelId") + .HasColumnType("uuid"); + + b1.Property("City") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("city"); + + b1.Property("Country") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("country"); + + b1.Property("State") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("state"); + + b1.Property("Street") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("street"); + + b1.Property("ZipCode") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("zip_code"); + + b1.HasKey("HotelId"); + + b1.ToTable("hotels"); + + b1.WithOwner() + .HasForeignKey("HotelId"); + }); + + b.Navigation("Address") + .IsRequired(); + }); + + modelBuilder.Entity("FC4.HotelReservation.Catalog.Domain.Entities.Room", b => + { + b.HasOne("FC4.HotelReservation.Catalog.Domain.Entities.Hotel", null) + .WithMany() + .HasForeignKey("HotelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_rooms_hotels"); + + b.HasOne("FC4.HotelReservation.Catalog.Domain.Entities.RoomType", null) + .WithMany() + .HasForeignKey("RoomTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_rooms_room_types"); + }); + + modelBuilder.Entity("FC4.HotelReservation.Catalog.Domain.Entities.RoomTypeRate", b => + { + b.HasOne("FC4.HotelReservation.Catalog.Domain.Entities.Hotel", null) + .WithMany() + .HasForeignKey("HotelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_room_type_rates_hotels"); + + b.HasOne("FC4.HotelReservation.Catalog.Domain.Entities.RoomType", null) + .WithMany() + .HasForeignKey("RoomTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_room_type_rates_room_types"); + + b.OwnsOne("FC4.HotelReservation.Catalog.Domain.ValueObjects.Money", "Rate", b1 => + { + b1.Property("RoomTypeRateId") + .HasColumnType("uuid"); + + b1.Property("Currency") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("character varying(3)") + .HasColumnName("rate_currency"); + + b1.Property("Value") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasColumnName("rate_amount"); + + b1.HasKey("RoomTypeRateId"); + + b1.ToTable("room_type_rates"); + + b1.WithOwner() + .HasForeignKey("RoomTypeRateId"); + }); + + b.Navigation("Rate") + .IsRequired(); + }); + + modelBuilder.Entity("FC4.HotelReservation.Guests.Domain.Entities.Guest", b => + { + b.OwnsOne("FC4.HotelReservation.Guests.Domain.ValueObjects.Email", "Email", b1 => + { + b1.Property("GuestId") + .HasColumnType("uuid"); + + b1.Property("Value") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("email"); + + b1.HasKey("GuestId"); + + b1.ToTable("guests"); + + b1.WithOwner() + .HasForeignKey("GuestId"); + }); + + b.Navigation("Email") + .IsRequired(); + }); + + modelBuilder.Entity("FC4.HotelReservation.Payments.Domain.Entities.Payment", b => + { + b.HasOne("FC4.HotelReservation.Reservations.Domain.Entities.Reservation", null) + .WithMany() + .HasForeignKey("ReservationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_payments_reservations"); + + b.OwnsOne("FC4.HotelReservation.Payments.Domain.ValueObjects.Money", "Amount", b1 => + { + b1.Property("PaymentId") + .HasColumnType("uuid"); + + b1.Property("Currency") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("character varying(3)") + .HasColumnName("currency"); + + b1.Property("Value") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasColumnName("amount"); + + b1.HasKey("PaymentId"); + + b1.ToTable("payments"); + + b1.WithOwner() + .HasForeignKey("PaymentId"); + }); + + b.Navigation("Amount") + .IsRequired(); + }); + + modelBuilder.Entity("FC4.HotelReservation.Reservations.Domain.Entities.Reservation", b => + { + b.HasOne("FC4.HotelReservation.Guests.Domain.Entities.Guest", null) + .WithMany() + .HasForeignKey("GuestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_reservations_guests"); + + b.HasOne("FC4.HotelReservation.Catalog.Domain.Entities.Hotel", null) + .WithMany() + .HasForeignKey("HotelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_reservations_hotels"); + + b.HasOne("FC4.HotelReservation.Catalog.Domain.Entities.RoomType", null) + .WithMany() + .HasForeignKey("RoomTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_reservations_room_types"); + + b.OwnsOne("FC4.HotelReservation.Reservations.Domain.ValueObjects.DateRange", "StayPeriod", b1 => + { + b1.Property("ReservationId") + .HasColumnType("uuid"); + + b1.Property("EndDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("stay_end_date"); + + b1.Property("StartDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("stay_start_date"); + + b1.HasKey("ReservationId"); + + b1.ToTable("reservations"); + + b1.WithOwner() + .HasForeignKey("ReservationId"); + }); + + b.OwnsOne("FC4.HotelReservation.Reservations.Domain.ValueObjects.Money", "TotalAmount", b1 => + { + b1.Property("ReservationId") + .HasColumnType("uuid"); + + b1.Property("Currency") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("character varying(3)") + .HasColumnName("total_currency"); + + b1.Property("Value") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasColumnName("total_amount"); + + b1.HasKey("ReservationId"); + + b1.ToTable("reservations"); + + b1.WithOwner() + .HasForeignKey("ReservationId"); + }); + + b.Navigation("StayPeriod") + .IsRequired(); + + b.Navigation("TotalAmount") + .IsRequired(); + }); + + modelBuilder.Entity("FC4.HotelReservation.Reservations.Domain.Entities.RoomTypeInventory", b => + { + b.HasOne("FC4.HotelReservation.Catalog.Domain.Entities.Hotel", null) + .WithMany() + .HasForeignKey("HotelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_room_type_inventories_hotels"); + + b.HasOne("FC4.HotelReservation.Catalog.Domain.Entities.RoomType", null) + .WithMany() + .HasForeignKey("RoomTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_room_type_inventories_room_types"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/FC4.HotelReservation/modular monolith/shared/FC4.HotelReservation.Shared.Infrastructure/Migrations/20260309161251_RowVersion.cs b/FC4.HotelReservation/modular monolith/shared/FC4.HotelReservation.Shared.Infrastructure/Migrations/20260309161251_RowVersion.cs new file mode 100644 index 0000000..f0caab8 --- /dev/null +++ b/FC4.HotelReservation/modular monolith/shared/FC4.HotelReservation.Shared.Infrastructure/Migrations/20260309161251_RowVersion.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace FC4.HotelReservation.Shared.Infrastructure.Migrations +{ + /// + public partial class RowVersion : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "version", + table: "room_type_inventory", + type: "integer", + nullable: false, + defaultValue: 0); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "version", + table: "room_type_inventory"); + } + } +} diff --git a/FC4.HotelReservation/modular monolith/shared/FC4.HotelReservation.Shared.Infrastructure/Migrations/HotelDbContextModelSnapshot.cs b/FC4.HotelReservation/modular monolith/shared/FC4.HotelReservation.Shared.Infrastructure/Migrations/HotelDbContextModelSnapshot.cs index 62766cb..d568232 100644 --- a/FC4.HotelReservation/modular monolith/shared/FC4.HotelReservation.Shared.Infrastructure/Migrations/HotelDbContextModelSnapshot.cs +++ b/FC4.HotelReservation/modular monolith/shared/FC4.HotelReservation.Shared.Infrastructure/Migrations/HotelDbContextModelSnapshot.cs @@ -22,47 +22,129 @@ protected override void BuildModel(ModelBuilder modelBuilder) NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - modelBuilder.Entity("FC4.HotelReservation.Reservations.Domain.Entities.Guest", b => + modelBuilder.Entity("FC4.HotelReservation.Catalog.Domain.Entities.Hotel", b => { b.Property("Id") .HasColumnType("uuid") .HasColumnName("id"); - b.Property("FirstName") + b.Property("Name") .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)") - .HasColumnName("first_name"); + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); - b.Property("LastName") + b.HasKey("Id"); + + b.ToTable("hotels", (string)null); + }); + + modelBuilder.Entity("FC4.HotelReservation.Catalog.Domain.Entities.Room", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Floor") + .HasColumnType("integer") + .HasColumnName("floor"); + + b.Property("HotelId") + .HasColumnType("uuid") + .HasColumnName("hotel_id"); + + b.Property("IsAvailable") + .HasColumnType("boolean") + .HasColumnName("is_available"); + + b.Property("Number") .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)") - .HasColumnName("last_name"); + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("number"); + + b.Property("RoomTypeId") + .HasColumnType("uuid") + .HasColumnName("room_type_id"); b.HasKey("Id"); - b.ToTable("guests", (string)null); + b.HasIndex("HotelId"); + + b.HasIndex("RoomTypeId"); + + b.ToTable("rooms", (string)null); }); - modelBuilder.Entity("FC4.HotelReservation.Reservations.Domain.Entities.Hotel", b => + modelBuilder.Entity("FC4.HotelReservation.Catalog.Domain.Entities.RoomType", b => { b.Property("Id") .HasColumnType("uuid") .HasColumnName("id"); - b.Property("Name") + b.Property("Description") .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)") - .HasColumnName("name"); + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("description"); b.HasKey("Id"); - b.ToTable("hotels", (string)null); + b.ToTable("room_types", (string)null); + }); + + modelBuilder.Entity("FC4.HotelReservation.Catalog.Domain.Entities.RoomTypeRate", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Date") + .HasColumnType("timestamp with time zone") + .HasColumnName("date"); + + b.Property("HotelId") + .HasColumnType("uuid") + .HasColumnName("hotel_id"); + + b.Property("RoomTypeId") + .HasColumnType("uuid") + .HasColumnName("room_type_id"); + + b.HasKey("Id"); + + b.HasIndex("RoomTypeId"); + + b.HasIndex("HotelId", "RoomTypeId", "Date") + .IsUnique(); + + b.ToTable("room_type_rates", (string)null); + }); + + modelBuilder.Entity("FC4.HotelReservation.Guests.Domain.Entities.Guest", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("first_name"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("last_name"); + + b.HasKey("Id"); + + b.ToTable("guests", (string)null); }); - modelBuilder.Entity("FC4.HotelReservation.Reservations.Domain.Entities.Payment", b => + modelBuilder.Entity("FC4.HotelReservation.Payments.Domain.Entities.Payment", b => { b.Property("Id") .HasColumnType("uuid") @@ -135,60 +217,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("reservations", (string)null); }); - modelBuilder.Entity("FC4.HotelReservation.Reservations.Domain.Entities.Room", b => - { - b.Property("Id") - .HasColumnType("uuid") - .HasColumnName("id"); - - b.Property("Floor") - .HasColumnType("integer") - .HasColumnName("floor"); - - b.Property("HotelId") - .HasColumnType("uuid") - .HasColumnName("hotel_id"); - - b.Property("IsAvailable") - .HasColumnType("boolean") - .HasColumnName("is_available"); - - b.Property("Number") - .IsRequired() - .HasMaxLength(10) - .HasColumnType("character varying(10)") - .HasColumnName("number"); - - b.Property("RoomTypeId") - .HasColumnType("uuid") - .HasColumnName("room_type_id"); - - b.HasKey("Id"); - - b.HasIndex("HotelId"); - - b.HasIndex("RoomTypeId"); - - b.ToTable("rooms", (string)null); - }); - - modelBuilder.Entity("FC4.HotelReservation.Reservations.Domain.Entities.RoomType", b => - { - b.Property("Id") - .HasColumnType("uuid") - .HasColumnName("id"); - - b.Property("Description") - .IsRequired() - .HasMaxLength(500) - .HasColumnType("character varying(500)") - .HasColumnName("description"); - - b.HasKey("Id"); - - b.ToTable("room_types", (string)null); - }); - modelBuilder.Entity("FC4.HotelReservation.Reservations.Domain.Entities.RoomTypeInventory", b => { b.Property("Id") @@ -215,33 +243,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("integer") .HasColumnName("total_reserved"); - b.HasKey("Id"); - - b.HasIndex("RoomTypeId"); - - b.HasIndex("HotelId", "RoomTypeId", "Date") - .IsUnique(); - - b.ToTable("room_type_inventory", (string)null); - }); - - modelBuilder.Entity("FC4.HotelReservation.Reservations.Domain.Entities.RoomTypeRate", b => - { - b.Property("Id") - .HasColumnType("uuid") - .HasColumnName("id"); - - b.Property("Date") - .HasColumnType("timestamp with time zone") - .HasColumnName("date"); - - b.Property("HotelId") - .HasColumnType("uuid") - .HasColumnName("hotel_id"); - - b.Property("RoomTypeId") - .HasColumnType("uuid") - .HasColumnName("room_type_id"); + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("integer") + .HasColumnName("version"); b.HasKey("Id"); @@ -250,37 +255,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("HotelId", "RoomTypeId", "Date") .IsUnique(); - b.ToTable("room_type_rates", (string)null); - }); - - modelBuilder.Entity("FC4.HotelReservation.Reservations.Domain.Entities.Guest", b => - { - b.OwnsOne("FC4.HotelReservation.Reservations.Domain.ValueObjects.Email", "Email", b1 => - { - b1.Property("GuestId") - .HasColumnType("uuid"); - - b1.Property("Value") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)") - .HasColumnName("email"); - - b1.HasKey("GuestId"); - - b1.ToTable("guests"); - - b1.WithOwner() - .HasForeignKey("GuestId"); - }); - - b.Navigation("Email") - .IsRequired(); + b.ToTable("room_type_inventory", (string)null); }); - modelBuilder.Entity("FC4.HotelReservation.Reservations.Domain.Entities.Hotel", b => + modelBuilder.Entity("FC4.HotelReservation.Catalog.Domain.Entities.Hotel", b => { - b.OwnsOne("FC4.HotelReservation.Reservations.Domain.ValueObjects.Address", "Address", b1 => + b.OwnsOne("FC4.HotelReservation.Catalog.Domain.ValueObjects.Address", "Address", b1 => { b1.Property("HotelId") .HasColumnType("uuid"); @@ -327,7 +307,93 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired(); }); - modelBuilder.Entity("FC4.HotelReservation.Reservations.Domain.Entities.Payment", b => + modelBuilder.Entity("FC4.HotelReservation.Catalog.Domain.Entities.Room", b => + { + b.HasOne("FC4.HotelReservation.Catalog.Domain.Entities.Hotel", null) + .WithMany() + .HasForeignKey("HotelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_rooms_hotels"); + + b.HasOne("FC4.HotelReservation.Catalog.Domain.Entities.RoomType", null) + .WithMany() + .HasForeignKey("RoomTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_rooms_room_types"); + }); + + modelBuilder.Entity("FC4.HotelReservation.Catalog.Domain.Entities.RoomTypeRate", b => + { + b.HasOne("FC4.HotelReservation.Catalog.Domain.Entities.Hotel", null) + .WithMany() + .HasForeignKey("HotelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_room_type_rates_hotels"); + + b.HasOne("FC4.HotelReservation.Catalog.Domain.Entities.RoomType", null) + .WithMany() + .HasForeignKey("RoomTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_room_type_rates_room_types"); + + b.OwnsOne("FC4.HotelReservation.Catalog.Domain.ValueObjects.Money", "Rate", b1 => + { + b1.Property("RoomTypeRateId") + .HasColumnType("uuid"); + + b1.Property("Currency") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("character varying(3)") + .HasColumnName("rate_currency"); + + b1.Property("Value") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasColumnName("rate_amount"); + + b1.HasKey("RoomTypeRateId"); + + b1.ToTable("room_type_rates"); + + b1.WithOwner() + .HasForeignKey("RoomTypeRateId"); + }); + + b.Navigation("Rate") + .IsRequired(); + }); + + modelBuilder.Entity("FC4.HotelReservation.Guests.Domain.Entities.Guest", b => + { + b.OwnsOne("FC4.HotelReservation.Guests.Domain.ValueObjects.Email", "Email", b1 => + { + b1.Property("GuestId") + .HasColumnType("uuid"); + + b1.Property("Value") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("email"); + + b1.HasKey("GuestId"); + + b1.ToTable("guests"); + + b1.WithOwner() + .HasForeignKey("GuestId"); + }); + + b.Navigation("Email") + .IsRequired(); + }); + + modelBuilder.Entity("FC4.HotelReservation.Payments.Domain.Entities.Payment", b => { b.HasOne("FC4.HotelReservation.Reservations.Domain.Entities.Reservation", null) .WithMany() @@ -336,7 +402,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasConstraintName("fk_payments_reservations"); - b.OwnsOne("FC4.HotelReservation.Reservations.Domain.ValueObjects.Money", "Amount", b1 => + b.OwnsOne("FC4.HotelReservation.Payments.Domain.ValueObjects.Money", "Amount", b1 => { b1.Property("PaymentId") .HasColumnType("uuid"); @@ -366,42 +432,39 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("FC4.HotelReservation.Reservations.Domain.Entities.Reservation", b => { - b.HasOne("FC4.HotelReservation.Reservations.Domain.Entities.Guest", null) + b.HasOne("FC4.HotelReservation.Guests.Domain.Entities.Guest", null) .WithMany() .HasForeignKey("GuestId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() .HasConstraintName("fk_reservations_guests"); - b.HasOne("FC4.HotelReservation.Reservations.Domain.Entities.Hotel", null) + b.HasOne("FC4.HotelReservation.Catalog.Domain.Entities.Hotel", null) .WithMany() .HasForeignKey("HotelId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() .HasConstraintName("fk_reservations_hotels"); - b.HasOne("FC4.HotelReservation.Reservations.Domain.Entities.RoomType", null) + b.HasOne("FC4.HotelReservation.Catalog.Domain.Entities.RoomType", null) .WithMany() .HasForeignKey("RoomTypeId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() .HasConstraintName("fk_reservations_room_types"); - b.OwnsOne("FC4.HotelReservation.Reservations.Domain.ValueObjects.Money", "TotalAmount", b1 => + b.OwnsOne("FC4.HotelReservation.Reservations.Domain.ValueObjects.DateRange", "StayPeriod", b1 => { b1.Property("ReservationId") .HasColumnType("uuid"); - b1.Property("Currency") - .IsRequired() - .HasMaxLength(3) - .HasColumnType("character varying(3)") - .HasColumnName("total_currency"); + b1.Property("EndDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("stay_end_date"); - b1.Property("Value") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)") - .HasColumnName("total_amount"); + b1.Property("StartDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("stay_start_date"); b1.HasKey("ReservationId"); @@ -411,18 +474,21 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasForeignKey("ReservationId"); }); - b.OwnsOne("FC4.HotelReservation.Reservations.Domain.ValueObjects.DateRange", "StayPeriod", b1 => + b.OwnsOne("FC4.HotelReservation.Reservations.Domain.ValueObjects.Money", "TotalAmount", b1 => { b1.Property("ReservationId") .HasColumnType("uuid"); - b1.Property("EndDate") - .HasColumnType("timestamp with time zone") - .HasColumnName("stay_end_date"); + b1.Property("Currency") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("character varying(3)") + .HasColumnName("total_currency"); - b1.Property("StartDate") - .HasColumnType("timestamp with time zone") - .HasColumnName("stay_start_date"); + b1.Property("Value") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasColumnName("total_amount"); b1.HasKey("ReservationId"); @@ -439,83 +505,22 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired(); }); - modelBuilder.Entity("FC4.HotelReservation.Reservations.Domain.Entities.Room", b => - { - b.HasOne("FC4.HotelReservation.Reservations.Domain.Entities.Hotel", null) - .WithMany() - .HasForeignKey("HotelId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_rooms_hotels"); - - b.HasOne("FC4.HotelReservation.Reservations.Domain.Entities.RoomType", null) - .WithMany() - .HasForeignKey("RoomTypeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_rooms_room_types"); - }); - modelBuilder.Entity("FC4.HotelReservation.Reservations.Domain.Entities.RoomTypeInventory", b => { - b.HasOne("FC4.HotelReservation.Reservations.Domain.Entities.Hotel", null) + b.HasOne("FC4.HotelReservation.Catalog.Domain.Entities.Hotel", null) .WithMany() .HasForeignKey("HotelId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() .HasConstraintName("fk_room_type_inventories_hotels"); - b.HasOne("FC4.HotelReservation.Reservations.Domain.Entities.RoomType", null) + b.HasOne("FC4.HotelReservation.Catalog.Domain.Entities.RoomType", null) .WithMany() .HasForeignKey("RoomTypeId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() .HasConstraintName("fk_room_type_inventories_room_types"); }); - - modelBuilder.Entity("FC4.HotelReservation.Reservations.Domain.Entities.RoomTypeRate", b => - { - b.HasOne("FC4.HotelReservation.Reservations.Domain.Entities.Hotel", null) - .WithMany() - .HasForeignKey("HotelId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_room_type_rates_hotels"); - - b.HasOne("FC4.HotelReservation.Reservations.Domain.Entities.RoomType", null) - .WithMany() - .HasForeignKey("RoomTypeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_room_type_rates_room_types"); - - b.OwnsOne("FC4.HotelReservation.Reservations.Domain.ValueObjects.Money", "Rate", b1 => - { - b1.Property("RoomTypeRateId") - .HasColumnType("uuid"); - - b1.Property("Currency") - .IsRequired() - .HasMaxLength(3) - .HasColumnType("character varying(3)") - .HasColumnName("rate_currency"); - - b1.Property("Value") - .HasPrecision(18, 2) - .HasColumnType("numeric(18,2)") - .HasColumnName("rate_amount"); - - b1.HasKey("RoomTypeRateId"); - - b1.ToTable("room_type_rates"); - - b1.WithOwner() - .HasForeignKey("RoomTypeRateId"); - }); - - b.Navigation("Rate") - .IsRequired(); - }); #pragma warning restore 612, 618 } } diff --git a/FC4.HotelReservation/modular monolith/shared/FC4.HotelReservation.Shared.Infrastructure/UnitOfWork.cs b/FC4.HotelReservation/modular monolith/shared/FC4.HotelReservation.Shared.Infrastructure/UnitOfWork.cs index 8e78fbf..df093c7 100644 --- a/FC4.HotelReservation/modular monolith/shared/FC4.HotelReservation.Shared.Infrastructure/UnitOfWork.cs +++ b/FC4.HotelReservation/modular monolith/shared/FC4.HotelReservation.Shared.Infrastructure/UnitOfWork.cs @@ -1,6 +1,8 @@ using FC4.HotelReservation.Shared.Application; +using FC4.HotelReservation.Shared.Application.Exceptions; using FC4.HotelReservation.Shared.Domain; using MediatR; +using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage; namespace FC4.HotelReservation.Shared.Infrastructure; @@ -10,7 +12,7 @@ public class UnitOfWork( IPublisher publisher) : IUnitOfWork { private readonly IDbContextTransaction _transaction = dbContext.Database.BeginTransaction(); - + public async Task CommitAsync(CancellationToken cancellationToken) { var aggregateRoots = dbContext @@ -28,9 +30,28 @@ public async Task CommitAsync(CancellationToken cancellationToken) aggregateRoot.RemoveEvent(@event); } } - - await dbContext.SaveChangesAsync(cancellationToken); - await _transaction.CommitAsync(cancellationToken); - await _transaction.DisposeAsync(); + + var versionedEntries = dbContext + .ChangeTracker + .Entries() + .Where(entry => entry.State == EntityState.Modified) + .ToList(); + + foreach (var entry in versionedEntries) + entry.Property(nameof(IVersioned.Version)).CurrentValue = entry.Entity.Version + 1; + + try + { + await dbContext.SaveChangesAsync(cancellationToken); + await _transaction.CommitAsync(cancellationToken); + } + catch (DbUpdateConcurrencyException ex) + { + throw new ConflictException("Concurrency conflict occurred during save operation", ex); + } + finally + { + await _transaction.DisposeAsync(); + } } } \ No newline at end of file diff --git a/FC4.HotelReservation/modular monolith/src/FC4.HotelReservation.WebApi/GlobalExceptionHandler.cs b/FC4.HotelReservation/modular monolith/src/FC4.HotelReservation.WebApi/GlobalExceptionHandler.cs index 6f990d8..541c678 100644 --- a/FC4.HotelReservation/modular monolith/src/FC4.HotelReservation.WebApi/GlobalExceptionHandler.cs +++ b/FC4.HotelReservation/modular monolith/src/FC4.HotelReservation.WebApi/GlobalExceptionHandler.cs @@ -27,6 +27,11 @@ public async ValueTask TryHandleAsync(HttpContext context, Exception excep problemDetails.Detail = exception.Message; problemDetails.Status = StatusCodes.Status404NotFound; break; + case ConflictException: + problemDetails.Title = "Resource Not Found"; + problemDetails.Detail = exception.Message; + problemDetails.Status = StatusCodes.Status409Conflict; + break; default: problemDetails.Title = "Unexpected Error"; problemDetails.Detail = "An unexpected error occurred.";