Files
baya-monorepo/server/src/Infrastructure/Baya.Infrastructure.Persistence/Configuration/BookingConfig/BookingConfig.cs
T
2026-08-02 23:42:12 +03:30

67 lines
3.7 KiB
C#

using Baya.Domain.Entities.Booking;
using Baya.Domain.Entities.Catalog;
using Baya.Domain.Entities.Identity;
using Baya.Infrastructure.Persistence.ValueConversion;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Baya.Infrastructure.Persistence.Configuration.BookingConfig;
internal sealed class BookingConfig : IEntityTypeConfiguration<Booking>
{
public void Configure(EntityTypeBuilder<Booking> builder)
{
// The three-amount split is a DB CHECK (not handler-only): gross = commission + payout, all ≥ 0.
builder.ToTable("Bookings", "booking", t => t.HasCheckConstraint(
"CK_Bookings_AmountSplit",
"[GrossPriceIrr] = [BalinyaarCommissionIrr] + [NursePayoutAmount] " +
"AND [GrossPriceIrr] >= 0 AND [BalinyaarCommissionIrr] >= 0 AND [NursePayoutAmount] >= 0"));
// Snapshots freeze history. AddressSnapshotJson is encrypted at rest (converter wired in
// ApplicationDbContext); both are nvarchar(max) so no length cap.
builder.Property(b => b.VariantSnapshotJson).IsRequired();
builder.Property(b => b.AddressSnapshotJson).IsRequired();
builder.Property(b => b.PlatformFeeRate).HasPrecision(5, 4);
builder.Property(b => b.CancellationRefundPercentage).HasPrecision(5, 2);
builder.Property(b => b.Status).HasMaxLength(30).IsRequired();
builder.Property(b => b.CancellationReason).HasMaxLength(500);
builder.Property(b => b.CancelledBy).HasMaxLength(20);
builder.Property(b => b.CancellationPolicyCode).HasMaxLength(50);
// datetime2 loses Kind on read (comes back Unspecified); re-tag as Utc so JSON serialization keeps
// the trailing Z and clients don't misparse these timeline instants as local time
// (mvp/blocker-phases/04's follow-up, folded in here since this phase already touches Booking timezone
// handling).
builder.Property(b => b.ConfirmedAt).HasConversion(new UtcDateTimeConverter());
builder.Property(b => b.CancelledAt).HasConversion(new UtcDateTimeConverter());
builder.Property(b => b.CompletedAt).HasConversion(new UtcDateTimeConverter());
builder.Property(b => b.DisputeWindowEndsAt).HasConversion(new UtcDateTimeConverter());
builder.HasIndex(b => new { b.CustomerId, b.Status });
builder.HasIndex(b => new { b.NurseId, b.Status });
// b13 selects payout-eligible bookings through the dispute-window close.
builder.HasIndex(b => b.DisputeWindowEndsAt);
// 1:1 with the request that created it (UNIQUE index created automatically). No navigation either
// side — the request is read by id at conversion time.
builder.HasOne<BookingRequest>()
.WithOne()
.HasForeignKey<Booking>(b => b.BookingRequestId)
.IsRequired();
// Denormalized FKs for query performance (no navigations on Booking). partner_center_id is left
// FK-less and nullable — partner_centers is DEFERRED to b15.
builder.HasOne<CustomerProfile>().WithMany().HasForeignKey(b => b.CustomerId).IsRequired();
builder.HasOne<NurseProfile>().WithMany().HasForeignKey(b => b.NurseId).IsRequired();
builder.HasOne<Patient>().WithMany().HasForeignKey(b => b.PatientId).IsRequired();
builder.HasOne<NurseServiceVariant>().WithMany().HasForeignKey(b => b.VariantId).IsRequired();
builder.HasOne<CustomerAddress>().WithMany().HasForeignKey(b => b.CustomerAddressId).IsRequired();
builder.HasMany(b => b.Sessions).WithOne(s => s.Booking).HasForeignKey(s => s.BookingId);
builder.HasQueryFilter(b => b.DeletedAt == null);
}
}