Files
amrez-nova-eop-services-api/AMREZ.EOP.Infrastructures/Data/AppDbContext.cs
Thanakarn Klangkasame 1e636aa3d5 [Add] MasterData Services.
2025-11-26 10:29:56 +07:00

887 lines
36 KiB
C#

using System.Text.Json;
using AMREZ.EOP.Domain.Entities.Authentications;
using AMREZ.EOP.Domain.Entities.Common;
using AMREZ.EOP.Domain.Entities.Customers;
using AMREZ.EOP.Domain.Entities.HumanResources;
using AMREZ.EOP.Domain.Entities.MasterData;
using AMREZ.EOP.Domain.Entities.Tenancy;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Route = AMREZ.EOP.Domain.Entities.MasterData.Route;
namespace AMREZ.EOP.Infrastructures.Data;
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
{
}
// ===== Auth =====
public DbSet<User> Users => Set<User>();
public DbSet<UserIdentity> UserIdentities => Set<UserIdentity>();
public DbSet<UserMfaFactor> UserMfaFactors => Set<UserMfaFactor>();
public DbSet<UserSession> UserSessions => Set<UserSession>();
public DbSet<UserPasswordHistory> UserPasswordHistories => Set<UserPasswordHistory>();
public DbSet<UserExternalAccount> UserExternalAccounts => Set<UserExternalAccount>();
public DbSet<Role> Roles => Set<Role>();
public DbSet<Permission> Permissions => Set<Permission>();
public DbSet<UserRole> UserRoles => Set<UserRole>();
public DbSet<RolePermission> RolePermissions => Set<RolePermission>();
// ===== HR =====
public DbSet<UserProfile> UserProfiles => Set<UserProfile>();
public DbSet<Department> Departments => Set<Department>();
public DbSet<Position> Positions => Set<Position>();
public DbSet<Employment> Employments => Set<Employment>();
public DbSet<EmployeeAddress> EmployeeAddresses => Set<EmployeeAddress>();
public DbSet<EmergencyContact> EmergencyContacts => Set<EmergencyContact>();
public DbSet<EmployeeBankAccount> EmployeeBankAccounts => Set<EmployeeBankAccount>();
// ===== Tenancy (meta) =====
public DbSet<TenantConfig> Tenants => Set<TenantConfig>();
public DbSet<TenantDomain> TenantDomains => Set<TenantDomain>();
// ===== Customers =====
public DbSet<CustomerProfile> CustomerProfiles => Set<CustomerProfile>();
public DbSet<PersonProfile> PersonProfiles => Set<PersonProfile>();
public DbSet<OrganizationProfile> OrganizationProfiles => Set<OrganizationProfile>();
public DbSet<Address> Addresses => Set<Address>();
public DbSet<CustomerAddress> CustomerAddresses => Set<CustomerAddress>();
public DbSet<CustomerContact> CustomerContacts => Set<CustomerContact>();
public DbSet<CustomerTag> CustomerTags => Set<CustomerTag>();
// ===== Master Data =====
public DbSet<Brand> Brands => Set<Brand>();
public DbSet<BrandBlock> BrandBlocks => Set<BrandBlock>();
public DbSet<Category> Categories => Set<Category>();
public DbSet<CategoryBlock> CategoryBlocks => Set<CategoryBlock>();
public DbSet<CategoryExt> CategoryExts => Set<CategoryExt>();
public DbSet<Uom> Uoms => Set<Uom>();
public DbSet<UomBlock> UomBlocks => Set<UomBlock>();
public DbSet<Currency> Currencies => Set<Currency>();
public DbSet<CurrencyBlock> CurrencyBlocks => Set<CurrencyBlock>();
public DbSet<Country> Countries => Set<Country>();
public DbSet<CountryBlock> CountryBlocks => Set<CountryBlock>();
public DbSet<Manufacturer> Manufacturers => Set<Manufacturer>();
public DbSet<ManufacturerBlock> ManufacturerBlocks => Set<ManufacturerBlock>();
public DbSet<Market> Markets => Set<Market>();
public DbSet<MarketBlock> MarketBlocks => Set<MarketBlock>();
public DbSet<Language> Languages => Set<Language>();
public DbSet<LanguageBlock> LanguageBlocks => Set<LanguageBlock>();
public DbSet<ComplianceStatus> ComplianceStatuses => Set<ComplianceStatus>();
public DbSet<ComplianceStatusBlock> ComplianceStatusBlocks => Set<ComplianceStatusBlock>();
public DbSet<QaStage> QaStages => Set<QaStage>();
public DbSet<QaStageBlock> QaStageBlocks => Set<QaStageBlock>();
public DbSet<Route> Routes => Set<Route>();
public DbSet<RouteBlock> RouteBlocks => Set<RouteBlock>();
public DbSet<RxSchedule> RxSchedules => Set<RxSchedule>();
public DbSet<RxScheduleBlock> RxScheduleBlocks => Set<RxScheduleBlock>();
public DbSet<StabilityStatus> StabilityStatuses => Set<StabilityStatus>();
public DbSet<StabilityStatusBlock> StabilityStatusBlocks => Set<StabilityStatusBlock>();
public DbSet<RiskClass> RiskClasses => Set<RiskClass>();
public DbSet<RiskClassBlock> RiskClassBlocks => Set<RiskClassBlock>();
public DbSet<SterilizationMethod> SterilizationMethods => Set<SterilizationMethod>();
public DbSet<SterilizationMethodBlock> SterilizationMethodBlocks => Set<SterilizationMethodBlock>();
public DbSet<QcStatus> QcStatuses => Set<QcStatus>();
public DbSet<QcStatusBlock> QcStatusBlocks => Set<QcStatusBlock>();
public DbSet<DocControlStatus> DocControlStatuses => Set<DocControlStatus>();
public DbSet<DocControlStatusBlock> DocControlStatusBlocks => Set<DocControlStatusBlock>();
public DbSet<RecallClass> RecallClasses => Set<RecallClass>();
public DbSet<RecallClassBlock> RecallClassBlocks => Set<RecallClassBlock>();
public DbSet<PackingGroup> PackingGroups => Set<PackingGroup>();
public DbSet<PackingGroupBlock> PackingGroupBlocks => Set<PackingGroupBlock>();
public DbSet<Vvm> Vvms => Set<Vvm>();
public DbSet<VvmBlock> VvmBlocks => Set<VvmBlock>();
public DbSet<FuncTest> FuncTests => Set<FuncTest>();
public DbSet<FuncTestBlock> FuncTestBlocks => Set<FuncTestBlock>();
public DbSet<HazardClass> HazardClasses => Set<HazardClass>();
public DbSet<HazardClassBlock> HazardClassBlocks => Set<HazardClassBlock>();
public DbSet<Species> Species => Set<Species>();
public DbSet<SpeciesBlock> SpeciesBlocks => Set<SpeciesBlock>();
public DbSet<Allergen> Allergens => Set<Allergen>();
public DbSet<AllergenBlock> AllergenBlocks => Set<AllergenBlock>();
public DbSet<Province> Provinces => Set<Province>();
public DbSet<ProvinceBlock> ProvinceBlocks => Set<ProvinceBlock>();
public DbSet<District> Districts => Set<District>();
public DbSet<DistrictBlock> DistrictBlocks => Set<DistrictBlock>();
public DbSet<Subdistrict> Subdistricts => Set<Subdistrict>();
public DbSet<SubdistrictBlock> SubdistrictBlocks => Set<SubdistrictBlock>();
protected override void OnModelCreating(ModelBuilder model)
{
// JSON converters/comparers
var jsonOpts = new JsonSerializerOptions(JsonSerializerDefaults.Web);
var dictStrConverter = new ValueConverter<Dictionary<string, string>?, string?>(
v => v == null ? null : JsonSerializer.Serialize(v, jsonOpts),
v => string.IsNullOrWhiteSpace(v)
? null
: JsonSerializer.Deserialize<Dictionary<string, string>>(v!, jsonOpts)!);
var dictStrComparer = new ValueComparer<Dictionary<string, string>?>(
(l, r) => JsonSerializer.Serialize(l, jsonOpts) == JsonSerializer.Serialize(r, jsonOpts),
v => v == null ? 0 : JsonSerializer.Serialize(v, jsonOpts).GetHashCode(),
v => v == null
? null
: JsonSerializer.Deserialize<Dictionary<string, string>>(JsonSerializer.Serialize(v, jsonOpts),
jsonOpts));
var dictObjConverter = new ValueConverter<Dictionary<string, object>?, string?>(
v => v == null ? null : JsonSerializer.Serialize(v, jsonOpts),
v => string.IsNullOrWhiteSpace(v)
? null
: JsonSerializer.Deserialize<Dictionary<string, object>>(v!, jsonOpts)!);
var dictObjComparer = new ValueComparer<Dictionary<string, object>?>(
(l, r) => JsonSerializer.Serialize(l, jsonOpts) == JsonSerializer.Serialize(r, jsonOpts),
v => v == null ? 0 : JsonSerializer.Serialize(v, jsonOpts).GetHashCode(),
v => v == null
? null
: JsonSerializer.Deserialize<Dictionary<string, object>>(JsonSerializer.Serialize(v, jsonOpts),
jsonOpts));
void ConfigureMasterBase<TEntity>(string table, string schema = "master")
where TEntity : MasterBase
{
var b = model.Entity<TEntity>();
b.ToTable(table, schema);
b.HasKey(x => x.Id);
b.HasAlternateKey(x => new { x.TenantId, x.Id });
b.Property(x => x.Scope).IsRequired().HasDefaultValue("global");
b.Property(x => x.Code).IsRequired();
b.Property(x => x.Name).IsRequired();
b.Property(x => x.IsActive).HasDefaultValue(true);
b.Property(x => x.IsSystem).HasDefaultValue(false);
var nameI18nProp = b.Property(x => x.NameI18n).HasConversion(dictStrConverter);
nameI18nProp.Metadata.SetValueComparer(dictStrComparer);
nameI18nProp.HasColumnType("jsonb");
var metaProp = b.Property(x => x.Meta).HasConversion(dictObjConverter);
metaProp.Metadata.SetValueComparer(dictObjComparer);
metaProp.HasColumnType("jsonb");
b.HasIndex(x => new { x.Scope, x.TenantId, x.Code }).IsUnique();
}
void ConfigureBlockBase<TBlock, TMaster>(string table, string schema = "master")
where TBlock : MasterBlockBase
where TMaster : MasterBase
{
var b = model.Entity<TBlock>();
b.ToTable(table, schema);
b.HasKey(x => x.Id);
b.HasIndex(x => new { x.TenantId, x.GlobalId }).IsUnique();
b.HasIndex(x => x.GlobalId);
b.HasOne<TMaster>()
.WithMany()
.HasForeignKey(x => x.GlobalId)
.OnDelete(DeleteBehavior.Cascade);
}
// ====== Tenancy (meta) ======
model.Entity<TenantConfig>(b =>
{
b.ToTable("tenants", schema: "meta");
b.HasKey(x => x.TenantKey);
b.HasAlternateKey(x => x.TenantId);
b.HasIndex(x => x.TenantId).IsUnique();
b.Property(x => x.TenantKey).HasMaxLength(128).IsRequired();
b.Property(x => x.Schema).HasMaxLength(128);
b.Property(x => x.ConnectionString);
b.Property(x => x.Mode).IsRequired();
b.Property(x => x.IsActive).HasDefaultValue(true);
b.Property(x => x.UpdatedAtUtc).HasColumnName("updated_at_utc")
.HasDefaultValueSql("now() at time zone 'utc'");
b.HasIndex(x => x.IsActive);
});
model.Entity<TenantDomain>(b =>
{
b.ToTable("tenant_domains", schema: "meta");
b.HasKey(x => x.Domain);
b.Property(x => x.Domain).HasMaxLength(253).IsRequired();
b.Property(x => x.TenantKey).HasMaxLength(128);
b.Property(x => x.IsPlatformBaseDomain).HasDefaultValue(false);
b.Property(x => x.IsActive).HasDefaultValue(true);
b.Property(x => x.UpdatedAtUtc).HasColumnName("updated_at_utc")
.HasDefaultValueSql("now() at time zone 'utc'");
b.HasIndex(x => x.TenantKey);
b.HasIndex(x => x.IsPlatformBaseDomain);
b.HasIndex(x => x.IsActive);
b.HasOne<TenantConfig>()
.WithMany()
.HasForeignKey(x => x.TenantKey)
.OnDelete(DeleteBehavior.Cascade);
});
// ====== Auth ======
model.Entity<User>(b =>
{
b.ToTable("users");
b.HasKey(x => x.Id);
b.HasAlternateKey(u => new { u.TenantId, u.Id });
b.Property(x => x.PasswordHash).IsRequired();
b.Property(x => x.IsActive).HasDefaultValue(true);
b.Property(x => x.AccessFailedCount).HasDefaultValue(0);
b.Property(x => x.MfaEnabled).HasDefaultValue(false);
});
model.Entity<UserIdentity>(b =>
{
b.ToTable("user_identities");
b.HasKey(x => x.Id);
b.Property(x => x.Type).IsRequired();
b.Property(x => x.Identifier).IsRequired().HasMaxLength(256);
b.Property(x => x.IsPrimary).HasDefaultValue(false);
b.HasIndex(x => new { x.TenantId, x.Type, x.Identifier }).IsUnique();
b.HasIndex(x => new { x.TenantId, x.UserId, x.Type, x.IsPrimary })
.HasDatabaseName("ix_user_identity_primary_per_type");
b.HasOne(i => i.User)
.WithMany(u => u.Identities)
.HasForeignKey(i => new { i.TenantId, i.UserId })
.HasPrincipalKey(nameof(User.TenantId), nameof(User.Id))
.OnDelete(DeleteBehavior.Cascade);
});
model.Entity<UserMfaFactor>(b =>
{
b.ToTable("user_mfa_factors");
b.HasKey(x => x.Id);
b.Property(x => x.Type).IsRequired();
b.Property(x => x.Enabled).HasDefaultValue(true);
b.HasIndex(x => new { x.TenantId, x.UserId });
b.HasOne(x => x.User)
.WithMany(u => u.MfaFactors)
.HasForeignKey(x => new { x.TenantId, x.UserId })
.HasPrincipalKey(nameof(User.TenantId), nameof(User.Id))
.OnDelete(DeleteBehavior.Cascade);
});
model.Entity<UserSession>(b =>
{
b.ToTable("user_sessions");
b.HasKey(x => x.Id);
b.Property(x => x.RefreshTokenHash).IsRequired();
b.HasIndex(x => new { x.TenantId, x.UserId });
b.HasIndex(x => new { x.TenantId, x.DeviceId });
b.HasOne(x => x.User)
.WithMany(u => u.Sessions)
.HasForeignKey(x => new { x.TenantId, x.UserId })
.HasPrincipalKey(nameof(User.TenantId), nameof(User.Id))
.OnDelete(DeleteBehavior.Cascade);
});
model.Entity<UserPasswordHistory>(b =>
{
b.ToTable("user_password_histories");
b.HasKey(x => x.Id);
b.Property(x => x.PasswordHash).IsRequired();
b.HasIndex(x => new { x.TenantId, x.UserId, x.ChangedAt });
b.HasOne(x => x.User)
.WithMany(u => u.PasswordHistories)
.HasForeignKey(x => new { x.TenantId, x.UserId })
.HasPrincipalKey(nameof(User.TenantId), nameof(User.Id))
.OnDelete(DeleteBehavior.Cascade);
});
model.Entity<UserExternalAccount>(b =>
{
b.ToTable("user_external_accounts");
b.HasKey(x => x.Id);
b.Property(x => x.Provider).IsRequired();
b.Property(x => x.Subject).IsRequired();
b.HasIndex(x => new { x.TenantId, x.Provider, x.Subject }).IsUnique();
b.HasOne(x => x.User)
.WithMany(u => u.ExternalAccounts)
.HasForeignKey(x => new { x.TenantId, x.UserId })
.HasPrincipalKey(nameof(User.TenantId), nameof(User.Id))
.OnDelete(DeleteBehavior.Cascade);
});
model.Entity<Role>(b =>
{
b.ToTable("roles");
b.HasKey(x => x.Id);
b.HasAlternateKey(r => new { r.TenantId, r.Id });
b.Property(x => x.Code).IsRequired().HasMaxLength(128);
b.Property(x => x.Name).IsRequired().HasMaxLength(256);
b.HasIndex(x => new { x.TenantId, x.Code }).IsUnique();
});
model.Entity<Permission>(b =>
{
b.ToTable("permissions");
b.HasKey(x => x.Id);
b.HasAlternateKey(p => new { p.TenantId, p.Id });
b.Property(x => x.Code).IsRequired().HasMaxLength(256);
b.Property(x => x.Name).IsRequired().HasMaxLength(256);
b.HasIndex(x => new { x.TenantId, x.Code }).IsUnique();
});
model.Entity<UserRole>(b =>
{
b.ToTable("user_roles");
b.HasKey(x => x.Id);
b.HasIndex(x => new { x.TenantId, x.UserId, x.RoleId }).IsUnique();
b.HasOne<User>()
.WithMany()
.HasForeignKey(x => new { x.TenantId, x.UserId })
.HasPrincipalKey(nameof(User.TenantId), nameof(User.Id))
.OnDelete(DeleteBehavior.Cascade);
b.HasOne<Role>()
.WithMany()
.HasForeignKey(x => new { x.TenantId, x.RoleId })
.HasPrincipalKey(nameof(Role.TenantId), nameof(Role.Id))
.OnDelete(DeleteBehavior.Cascade);
});
model.Entity<RolePermission>(b =>
{
b.ToTable("role_permissions");
b.HasKey(x => x.Id);
b.HasIndex(x => new { x.TenantId, x.RoleId, x.PermissionId }).IsUnique();
b.HasOne<Role>()
.WithMany()
.HasForeignKey(x => new { x.TenantId, x.RoleId })
.HasPrincipalKey(nameof(Role.TenantId), nameof(Role.Id))
.OnDelete(DeleteBehavior.Cascade);
b.HasOne<Permission>()
.WithMany()
.HasForeignKey(x => new { x.TenantId, x.PermissionId })
.HasPrincipalKey(nameof(Permission.TenantId), nameof(Permission.Id))
.OnDelete(DeleteBehavior.Cascade);
});
// ====== HR ======
model.Entity<UserProfile>(b =>
{
b.ToTable("user_profiles");
b.HasKey(x => x.Id);
b.HasAlternateKey(p => new { p.TenantId, p.Id });
b.Property(x => x.FirstName).IsRequired().HasMaxLength(128);
b.Property(x => x.LastName).IsRequired().HasMaxLength(128);
b.HasIndex(x => new { x.TenantId, x.UserId }).IsUnique();
b.HasOne(x => x.User)
.WithOne()
.HasForeignKey<UserProfile>(x => new { x.TenantId, x.UserId })
.HasPrincipalKey<User>(u => new { u.TenantId, u.Id })
.OnDelete(DeleteBehavior.Cascade);
});
model.Entity<Department>(b =>
{
b.ToTable("departments");
b.HasKey(x => x.Id);
b.HasAlternateKey(d => new { d.TenantId, d.Id });
b.Property(x => x.Code).IsRequired().HasMaxLength(64);
b.Property(x => x.Name).IsRequired().HasMaxLength(256);
b.HasIndex(x => new { x.TenantId, x.Code }).IsUnique();
b.HasOne(x => x.Parent)
.WithMany(x => x.Children)
.HasForeignKey(x => new { x.TenantId, x.ParentDepartmentId })
.HasPrincipalKey(nameof(Department.TenantId), nameof(Department.Id))
.OnDelete(DeleteBehavior.Restrict);
});
model.Entity<Position>(b =>
{
b.ToTable("positions");
b.HasKey(x => x.Id);
b.HasAlternateKey(p => new { p.TenantId, p.Id });
b.Property(x => x.Code).IsRequired().HasMaxLength(64);
b.Property(x => x.Title).IsRequired().HasMaxLength(256);
b.HasIndex(x => new { x.TenantId, x.Code }).IsUnique();
});
model.Entity<Employment>(b =>
{
b.ToTable("employments");
b.HasKey(x => x.Id);
b.Property(x => x.EmploymentType).IsRequired();
b.Property(x => x.StartDate).IsRequired();
b.HasIndex(x => new { x.TenantId, x.UserProfileId, x.StartDate });
b.HasOne(x => x.UserProfile)
.WithMany(p => p.Employments)
.HasForeignKey(x => new { x.TenantId, x.UserProfileId })
.HasPrincipalKey(nameof(UserProfile.TenantId), nameof(UserProfile.Id))
.OnDelete(DeleteBehavior.Cascade);
b.HasOne(x => x.Department)
.WithMany()
.HasForeignKey(x => new { x.TenantId, x.DepartmentId })
.HasPrincipalKey(nameof(Department.TenantId), nameof(Department.Id))
.OnDelete(DeleteBehavior.Restrict);
b.HasOne(x => x.Position)
.WithMany()
.HasForeignKey(x => new { x.TenantId, x.PositionId })
.HasPrincipalKey(nameof(Position.TenantId), nameof(Position.Id))
.OnDelete(DeleteBehavior.Restrict);
});
model.Entity<EmployeeAddress>(b =>
{
b.ToTable("employee_addresses");
b.HasKey(x => x.Id);
b.Property(x => x.Line1).IsRequired().HasMaxLength(256);
b.Property(x => x.City).IsRequired().HasMaxLength(128);
b.Property(x => x.PostalCode).IsRequired().HasMaxLength(32);
b.Property(x => x.Country).IsRequired().HasMaxLength(64);
b.HasOne(x => x.UserProfile)
.WithMany(p => p.Addresses)
.HasForeignKey(x => new { x.TenantId, x.UserProfileId })
.HasPrincipalKey(nameof(UserProfile.TenantId), nameof(UserProfile.Id))
.OnDelete(DeleteBehavior.Cascade);
b.HasIndex(x => new { x.TenantId, x.UserProfileId, x.IsPrimary });
});
model.Entity<EmergencyContact>(b =>
{
b.ToTable("emergency_contacts");
b.HasKey(x => x.Id);
b.Property(x => x.Name).IsRequired().HasMaxLength(128);
b.Property(x => x.Relationship).IsRequired().HasMaxLength(64);
b.HasOne(x => x.UserProfile)
.WithMany(p => p.EmergencyContacts)
.HasForeignKey(x => new { x.TenantId, x.UserProfileId })
.HasPrincipalKey(nameof(UserProfile.TenantId), nameof(UserProfile.Id))
.OnDelete(DeleteBehavior.Cascade);
b.HasIndex(x => new { x.TenantId, x.UserProfileId, x.IsPrimary });
});
model.Entity<EmployeeBankAccount>(b =>
{
b.ToTable("employee_bank_accounts");
b.HasKey(x => x.Id);
b.Property(x => x.BankName).IsRequired().HasMaxLength(128);
b.Property(x => x.AccountNumber).IsRequired().HasMaxLength(64);
b.Property(x => x.AccountHolder).IsRequired().HasMaxLength(128);
b.HasOne(x => x.UserProfile)
.WithMany(p => p.BankAccounts)
.HasForeignKey(x => new { x.TenantId, x.UserProfileId })
.HasPrincipalKey(nameof(UserProfile.TenantId), nameof(UserProfile.Id))
.OnDelete(DeleteBehavior.Cascade);
b.HasIndex(x => new { x.TenantId, x.UserProfileId, x.IsPrimary });
});
// ====== Customers ======
model.Entity<CustomerProfile>(b =>
{
b.ToTable("customer_profiles");
b.HasKey(x => x.Id);
b.HasAlternateKey(x => new { x.TenantId, x.Id });
b.Property(x => x.DisplayName).HasMaxLength(256).IsRequired();
b.Property(x => x.Type).IsRequired();
b.Property(x => x.Status).IsRequired();
b.HasIndex(x => new { x.TenantId, x.DisplayName });
b.HasMany(x => x.Contacts)
.WithOne(x => x.Customer)
.HasForeignKey(x => new { x.TenantId, x.CustomerProfileId })
.HasPrincipalKey(x => new { x.TenantId, x.Id })
.OnDelete(DeleteBehavior.Cascade);
b.HasMany(x => x.Addresses)
.WithOne(x => x.Customer)
.HasForeignKey(x => new { x.TenantId, x.CustomerProfileId })
.HasPrincipalKey(x => new { x.TenantId, x.Id })
.OnDelete(DeleteBehavior.Cascade);
b.HasMany(x => x.Tags)
.WithOne(x => x.Customer)
.HasForeignKey(x => new { x.TenantId, x.CustomerProfileId })
.HasPrincipalKey(x => new { x.TenantId, x.Id })
.OnDelete(DeleteBehavior.Cascade);
b.HasOne(x => x.Person)
.WithOne(x => x.Customer)
.HasForeignKey<PersonProfile>(x => new { x.TenantId, x.CustomerProfileId })
.HasPrincipalKey<CustomerProfile>(x => new { x.TenantId, x.Id })
.OnDelete(DeleteBehavior.Cascade);
b.HasOne(x => x.Organization)
.WithOne(x => x.Customer)
.HasForeignKey<OrganizationProfile>(x => new { x.TenantId, x.CustomerProfileId })
.HasPrincipalKey<CustomerProfile>(x => new { x.TenantId, x.Id })
.OnDelete(DeleteBehavior.Cascade);
});
model.Entity<PersonProfile>(b =>
{
b.ToTable("person_profiles");
b.HasKey(x => x.Id);
b.Property(x => x.Prefix).HasMaxLength(32);
b.Property(x => x.FirstNameTh).HasMaxLength(128);
b.Property(x => x.LastNameTh).HasMaxLength(128);
b.Property(x => x.FirstNameEn).HasMaxLength(128);
b.Property(x => x.LastNameEn).HasMaxLength(128);
b.Property(x => x.NationalId).HasMaxLength(13);
b.Property(x => x.PassportNo).HasMaxLength(32);
b.HasIndex(x => new { x.TenantId, x.CustomerProfileId }).IsUnique();
});
model.Entity<OrganizationProfile>(b =>
{
b.ToTable("organization_profiles");
b.HasKey(x => x.Id);
b.Property(x => x.LegalNameTh).HasMaxLength(256).IsRequired();
b.Property(x => x.LegalNameEn).HasMaxLength(256);
b.Property(x => x.RegistrationNo).HasMaxLength(64);
b.Property(x => x.TaxId13).HasMaxLength(13);
b.Property(x => x.BranchCode).HasMaxLength(5);
b.Property(x => x.CompanyType).HasMaxLength(64);
b.HasIndex(x => new { x.TenantId, x.CustomerProfileId }).IsUnique();
b.HasIndex(x => new { x.TenantId, x.TaxId13, x.BranchCode }).IsUnique()
.HasFilter("\"TaxId13\" IS NOT NULL");
});
model.Entity<CustomerContact>(b =>
{
b.ToTable("customer_contacts");
b.HasKey(x => x.Id);
b.Property(x => x.Type).IsRequired();
b.Property(x => x.Value).HasMaxLength(256).IsRequired();
b.Property(x => x.IsPrimary).HasDefaultValue(false);
b.Property(x => x.IsVerified).HasDefaultValue(false);
b.HasIndex(x => new { x.TenantId, x.CustomerProfileId, x.Type });
b.HasIndex(x => new { x.TenantId, x.CustomerProfileId, x.Type, x.IsPrimary })
.HasDatabaseName("ux_customer_contact_primary_per_type")
.IsUnique()
.HasFilter("\"IsPrimary\" = TRUE");
});
model.Entity<Address>(b =>
{
b.ToTable("addresses");
b.HasKey(x => x.Id);
b.HasAlternateKey(x => new { x.TenantId, x.Id });
b.Property(x => x.Line1).HasMaxLength(256);
b.Property(x => x.Line2).HasMaxLength(256);
b.Property(x => x.Village).HasMaxLength(128);
b.Property(x => x.Soi).HasMaxLength(128);
b.Property(x => x.Road).HasMaxLength(128);
b.Property(x => x.Subdistrict).HasMaxLength(128);
b.Property(x => x.District).HasMaxLength(128);
b.Property(x => x.Province).HasMaxLength(128);
b.Property(x => x.PostalCode).HasMaxLength(10);
b.Property(x => x.CountryCode).HasMaxLength(2).IsRequired();
});
model.Entity<CustomerAddress>(b =>
{
b.ToTable("customer_addresses");
b.HasKey(x => x.Id);
b.Property(x => x.Label).IsRequired();
b.Property(x => x.IsDefault).HasDefaultValue(false);
b.HasIndex(x => new { x.TenantId, x.CustomerProfileId, x.Label });
b.HasOne(x => x.Customer)
.WithMany(c => c.Addresses)
.HasForeignKey(x => new { x.TenantId, x.CustomerProfileId })
.HasPrincipalKey(c => new { c.TenantId, c.Id })
.OnDelete(DeleteBehavior.Cascade);
b.HasOne(x => x.Address)
.WithMany(a => a.CustomerLinks)
.HasForeignKey(x => new { x.TenantId, x.AddressId })
.HasPrincipalKey(a => new { a.TenantId, a.Id })
.OnDelete(DeleteBehavior.Cascade);
b.HasIndex(x => new { x.TenantId, x.CustomerProfileId, x.Label, x.IsDefault })
.HasDatabaseName("ux_default_customer_address_per_label")
.IsUnique()
.HasFilter("\"IsDefault\" = TRUE");
});
model.Entity<CustomerTag>(b =>
{
b.ToTable("customer_tags");
b.HasKey(x => x.Id);
b.Property(x => x.Tag).HasMaxLength(64).IsRequired();
b.HasIndex(x => new { x.TenantId, x.CustomerProfileId, x.Tag }).IsUnique();
});
// ===== Master: mapping =====
ConfigureMasterBase<Brand>("brands");
ConfigureBlockBase<BrandBlock, Brand>("brand_blocks");
model.Entity<Category>(b =>
{
ConfigureMasterBase<Category>("categories");
b.HasOne(x => x.Parent)
.WithMany(x => x.Children)
.HasForeignKey(x => new { x.TenantId, x.ParentId })
.HasPrincipalKey(x => new { x.TenantId, x.Id })
.OnDelete(DeleteBehavior.Restrict);
b.HasIndex(x => x.ParentId);
b.HasIndex(x => x.Path);
b.HasIndex(x => new { x.Scope, x.TenantId, x.SortOrder });
});
ConfigureBlockBase<CategoryBlock, Category>("category_blocks");
model.Entity<CategoryExt>(b =>
{
b.ToTable("category_exts", "master");
b.HasKey(x => x.CategoryId);
b.HasOne(x => x.Category).WithOne().HasForeignKey<CategoryExt>(x => x.CategoryId)
.OnDelete(DeleteBehavior.Cascade);
var chVisProp = b.Property(x => x.ChannelVisibility).HasConversion(dictObjConverter);
chVisProp.Metadata.SetValueComparer(dictObjComparer);
chVisProp.HasColumnType("jsonb");
var facetSchemaProp = b.Property(x => x.FacetSchema).HasConversion(dictObjConverter);
facetSchemaProp.Metadata.SetValueComparer(dictObjComparer);
facetSchemaProp.HasColumnType("jsonb");
});
ConfigureMasterBase<Uom>("uoms");
ConfigureBlockBase<UomBlock, Uom>("uom_blocks");
ConfigureMasterBase<Currency>("currencies");
ConfigureBlockBase<CurrencyBlock, Currency>("currency_blocks");
ConfigureMasterBase<Country>("countries");
ConfigureBlockBase<CountryBlock, Country>("country_blocks");
ConfigureMasterBase<Manufacturer>("manufacturers");
ConfigureBlockBase<ManufacturerBlock, Manufacturer>("manufacturer_blocks");
ConfigureMasterBase<Market>("markets");
ConfigureBlockBase<MarketBlock, Market>("market_blocks");
ConfigureMasterBase<Language>("languages");
ConfigureBlockBase<LanguageBlock, Language>("language_blocks");
ConfigureMasterBase<ComplianceStatus>("compliance_statuses");
ConfigureBlockBase<ComplianceStatusBlock, ComplianceStatus>("compliance_status_blocks");
ConfigureMasterBase<QaStage>("qa_stages");
ConfigureBlockBase<QaStageBlock, QaStage>("qa_stage_blocks");
ConfigureMasterBase<Route>("routes");
ConfigureBlockBase<RouteBlock, Route>("route_blocks");
ConfigureMasterBase<RxSchedule>("rx_schedules");
ConfigureBlockBase<RxScheduleBlock, RxSchedule>("rx_schedule_blocks");
ConfigureMasterBase<StabilityStatus>("stability_statuses");
ConfigureBlockBase<StabilityStatusBlock, StabilityStatus>("stability_status_blocks");
ConfigureMasterBase<RiskClass>("risk_classes");
ConfigureBlockBase<RiskClassBlock, RiskClass>("risk_class_blocks");
ConfigureMasterBase<SterilizationMethod>("sterilization_methods");
ConfigureBlockBase<SterilizationMethodBlock, SterilizationMethod>("sterilization_method_blocks");
ConfigureMasterBase<QcStatus>("qc_statuses");
ConfigureBlockBase<QcStatusBlock, QcStatus>("qc_status_blocks");
ConfigureMasterBase<DocControlStatus>("doc_control_statuses");
ConfigureBlockBase<DocControlStatusBlock, DocControlStatus>("doc_control_status_blocks");
ConfigureMasterBase<RecallClass>("recall_classes");
ConfigureBlockBase<RecallClassBlock, RecallClass>("recall_class_blocks");
ConfigureMasterBase<PackingGroup>("packing_groups");
ConfigureBlockBase<PackingGroupBlock, PackingGroup>("packing_group_blocks");
ConfigureMasterBase<Vvm>("vvms");
ConfigureBlockBase<VvmBlock, Vvm>("vvm_blocks");
ConfigureMasterBase<FuncTest>("func_tests");
ConfigureBlockBase<FuncTestBlock, FuncTest>("func_test_blocks");
ConfigureMasterBase<HazardClass>("hazard_classes");
ConfigureBlockBase<HazardClassBlock, HazardClass>("hazard_class_blocks");
ConfigureMasterBase<Species>("species");
ConfigureBlockBase<SpeciesBlock, Species>("species_blocks");
ConfigureMasterBase<Allergen>("allergens");
ConfigureBlockBase<AllergenBlock, Allergen>("allergen_blocks");
ConfigureMasterBase<Province>("provinces");
ConfigureBlockBase<ProvinceBlock, Province>("province_blocks");
model.Entity<Province>(b =>
{
b.Property(x => x.Code)
.IsRequired()
.HasMaxLength(2);
b.HasIndex(x => x.Code);
});
ConfigureMasterBase<District>("districts");
ConfigureBlockBase<DistrictBlock, District>("district_blocks");
model.Entity<District>(b =>
{
b.Property(x => x.Code)
.IsRequired()
.HasMaxLength(4);
b.HasIndex(x => x.Code);
b.HasOne(x => x.Province)
.WithMany(p => p.Districts)
.HasForeignKey(x => new { x.TenantId, x.ProvinceId })
.HasPrincipalKey(p => new { p.TenantId, p.Id })
.OnDelete(DeleteBehavior.Restrict);
b.HasIndex(x => new { x.TenantId, x.ProvinceId });
});
ConfigureMasterBase<Subdistrict>("subdistricts");
ConfigureBlockBase<SubdistrictBlock, Subdistrict>("subdistrict_blocks");
model.Entity<Subdistrict>(b =>
{
b.Property(x => x.Code)
.IsRequired()
.HasMaxLength(6);
b.Property(x => x.Postcode)
.HasMaxLength(10);
b.HasIndex(x => x.Code);
b.HasIndex(x => x.Postcode);
b.HasOne(x => x.District)
.WithMany(d => d.Subdistricts)
.HasForeignKey(x => new { x.TenantId, x.DistrictId })
.HasPrincipalKey(d => new { d.TenantId, d.Id })
.OnDelete(DeleteBehavior.Restrict);
b.HasIndex(x => new { x.TenantId, x.DistrictId });
});
// ====== Enum conversions ======
model.Entity<UserIdentity>().Property(x => x.Type).HasConversion<int>();
model.Entity<UserMfaFactor>().Property(x => x.Type).HasConversion<int>();
model.Entity<UserExternalAccount>().Property(x => x.Provider).HasConversion<int>();
model.Entity<Employment>().Property(x => x.EmploymentType).HasConversion<int>();
model.Entity<UserProfile>().Property(x => x.Gender).HasConversion<int?>();
model.Entity<CustomerProfile>().Property(x => x.Type).HasConversion<int>();
model.Entity<CustomerProfile>().Property(x => x.Status).HasConversion<int>();
model.Entity<CustomerContact>().Property(x => x.Type).HasConversion<int>();
model.Entity<CustomerAddress>().Property(x => x.Label).HasConversion<int>();
// ====== BaseEntity common mapping ======
foreach (var et in model.Model.GetEntityTypes().Where(t => typeof(BaseEntity).IsAssignableFrom(t.ClrType)))
{
var b = model.Entity(et.ClrType);
b.Property<Guid>(nameof(BaseEntity.TenantId))
.HasColumnName("tenant_id")
.HasColumnType("uuid")
.IsRequired()
.ValueGeneratedNever();
b.HasIndex(nameof(BaseEntity.TenantId));
var tn = et.GetTableName();
if (!string.IsNullOrEmpty(tn))
{
b.HasCheckConstraint($"ck_{tn}_tenant_not_null", "tenant_id is not null");
b.HasCheckConstraint($"ck_{tn}_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
}
b.Property<DateTimeOffset>(nameof(BaseEntity.CreatedAt)).HasColumnName("created_at")
.HasDefaultValueSql("now() at time zone 'utc'");
b.Property<DateTimeOffset?>(nameof(BaseEntity.UpdatedAt)).HasColumnName("updated_at");
b.Property<string?>(nameof(BaseEntity.CreatedBy)).HasColumnName("created_by");
b.Property<string?>(nameof(BaseEntity.UpdatedBy)).HasColumnName("updated_by");
b.Property<bool>(nameof(BaseEntity.IsDeleted)).HasColumnName("is_deleted").HasDefaultValue(false);
}
}
}