using Baya.Domain.Entities.Catalog; using Baya.Domain.Entities.Identity; using Baya.Domain.Entities.Search; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace Baya.Infrastructure.Persistence.Configuration.SearchConfig; internal sealed class NurseSearchIndexConfig : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ToTable("NurseSearchIndex", "search"); // Price is IRR Rials as BIGINT (long → bigint) — copied from the variant. No float money path. builder.Property(x => x.PriceUnit).HasMaxLength(20).IsRequired(); builder.Property(x => x.NurseGender).HasMaxLength(10); builder.Property(x => x.AverageRating).HasPrecision(3, 2); // The hot search path: filter on (is_searchable, category, city, district) then rating-sort + page. // INCLUDE the columns the projection reads so the filtered, sorted page is served straight from the // index with no key lookups (SQL Server; the INCLUDE annotation is ignored by other providers). builder.HasIndex(x => new { x.IsSearchable, x.ServiceCategoryId, x.CityId, x.DistrictId }) .IncludeProperties(x => new { x.Price, x.NurseGender, x.AverageRating, x.TotalReviews, x.NurseId, x.VariantId }) .HasDatabaseName("IX_NurseSearchIndex_Search"); // Exactly one live row per (variant × area) — the upsert target and anti-duplication backstop. // SQL Server treats NULLs as distinct, so a plain UNIQUE(variant, city, district) would wrongly allow // two "whole city" (NULL district) rows. Split into a filtered pair exactly like nurse_service_areas: // one enforces at most one whole-city row, the other enforces uniqueness of city+district rows. Both // exclude soft-deleted rows so a removed-then-recovered area re-inserts cleanly. builder.HasIndex(x => new { x.VariantId, x.CityId }) .IsUnique() .HasFilter("[DistrictId] IS NULL AND [DeletedAt] IS NULL") .HasDatabaseName("UX_NurseSearchIndex_Variant_City_WholeCity"); builder.HasIndex(x => new { x.VariantId, x.CityId, x.DistrictId }) .IsUnique() .HasFilter("[DistrictId] IS NOT NULL AND [DeletedAt] IS NULL") .HasDatabaseName("UX_NurseSearchIndex_Variant_City_District"); // A nurse-scoped rebuild / suspend / remove touches every row for one nurse — keep it cheap. builder.HasIndex(x => x.NurseId); builder.HasOne(x => x.Variant) .WithMany() .HasForeignKey(x => x.VariantId) .IsRequired(); builder.HasOne() .WithMany() .HasForeignKey(x => x.NurseId) .IsRequired(); builder.HasQueryFilter(x => x.DeletedAt == null); } }