Add Master Data

This commit is contained in:
Thanakarn Klangkasame
2025-10-10 16:22:06 +07:00
parent ad0d9e41ba
commit d4ab1cb592
55 changed files with 16058 additions and 197 deletions

View File

@@ -1,15 +1,22 @@
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.Metadata;
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) { }
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
{
}
// ===== Auth =====
public DbSet<User> Users => Set<User>();
@@ -36,14 +43,170 @@ public class AppDbContext : DbContext
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>();
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();
// b.HasIndex(x => new { x.TenantId, x.OverridesGlobalId }).IsUnique().HasFilter("\"OverridesGlobalId\" IS NOT NULL");
}
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); // PK = key (slug)
b.HasAlternateKey(x => x.TenantId); // AK = GUID
b.HasKey(x => x.TenantKey);
b.HasAlternateKey(x => x.TenantId);
b.HasIndex(x => x.TenantId).IsUnique();
b.Property(x => x.TenantKey).HasMaxLength(128).IsRequired();
@@ -51,8 +214,7 @@ public class AppDbContext : DbContext
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")
b.Property(x => x.UpdatedAtUtc).HasColumnName("updated_at_utc")
.HasDefaultValueSql("now() at time zone 'utc'");
b.HasIndex(x => x.IsActive);
});
@@ -62,11 +224,10 @@ public class AppDbContext : DbContext
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); // optional
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")
b.Property(x => x.UpdatedAtUtc).HasColumnName("updated_at_utc")
.HasDefaultValueSql("now() at time zone 'utc'");
b.HasIndex(x => x.TenantKey);
@@ -84,8 +245,6 @@ public class AppDbContext : DbContext
{
b.ToTable("users");
b.HasKey(x => x.Id);
// principal key สำหรับ composite FK จากลูก ๆ
b.HasAlternateKey(u => new { u.TenantId, u.Id });
b.Property(x => x.PasswordHash).IsRequired();
@@ -105,14 +264,13 @@ public class AppDbContext : DbContext
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");
.HasDatabaseName("ix_user_identity_primary_per_type");
// (TenantId, UserId) -> User.(TenantId, Id)
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);
.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 =>
@@ -125,10 +283,10 @@ public class AppDbContext : DbContext
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);
.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 =>
@@ -141,10 +299,10 @@ public class AppDbContext : DbContext
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);
.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 =>
@@ -155,10 +313,10 @@ public class AppDbContext : DbContext
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);
.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 =>
@@ -171,10 +329,10 @@ public class AppDbContext : DbContext
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);
.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 =>
@@ -209,16 +367,16 @@ public class AppDbContext : DbContext
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);
.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);
.WithMany()
.HasForeignKey(x => new { x.TenantId, x.RoleId })
.HasPrincipalKey(nameof(Role.TenantId), nameof(Role.Id))
.OnDelete(DeleteBehavior.Cascade);
});
model.Entity<RolePermission>(b =>
@@ -229,16 +387,16 @@ public class AppDbContext : DbContext
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);
.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);
.WithMany()
.HasForeignKey(x => new { x.TenantId, x.PermissionId })
.HasPrincipalKey(nameof(Permission.TenantId), nameof(Permission.Id))
.OnDelete(DeleteBehavior.Cascade);
});
// ====== HR ======
@@ -256,7 +414,7 @@ public class AppDbContext : DbContext
b.HasOne(x => x.User)
.WithOne()
.HasForeignKey<UserProfile>(x => new { x.TenantId, x.UserId })
.HasPrincipalKey<User>(u => new { u.TenantId, u.Id }) // <-- เปลี่ยนตรงนี้
.HasPrincipalKey<User>(u => new { u.TenantId, u.Id })
.OnDelete(DeleteBehavior.Cascade);
});
@@ -272,10 +430,10 @@ public class AppDbContext : DbContext
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);
.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 =>
@@ -301,22 +459,22 @@ public class AppDbContext : DbContext
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);
.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);
.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);
.WithMany()
.HasForeignKey(x => new { x.TenantId, x.PositionId })
.HasPrincipalKey(nameof(Position.TenantId), nameof(Position.Id))
.OnDelete(DeleteBehavior.Restrict);
});
model.Entity<EmployeeAddress>(b =>
@@ -330,10 +488,10 @@ public class AppDbContext : DbContext
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);
.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 });
});
@@ -347,10 +505,10 @@ public class AppDbContext : DbContext
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);
.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 });
});
@@ -365,28 +523,280 @@ public class AppDbContext : DbContext
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);
.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 });
});
// ====== Enums as ints ======
// ====== 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");
// ====== 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)))
foreach (var et in model.Model.GetEntityTypes().Where(t => typeof(BaseEntity).IsAssignableFrom(t.ClrType)))
{
var b = model.Entity(et.ClrType);
// Tenant
b.Property<Guid>(nameof(BaseEntity.TenantId))
.HasColumnName("tenant_id")
.HasColumnType("uuid")
@@ -395,23 +805,19 @@ public class AppDbContext : DbContext
b.HasIndex(nameof(BaseEntity.TenantId));
// ชื่อ constraint สร้างแบบ concat แทน string interpolation กัน ambiguous handler
var tn = et.GetTableName();
if (!string.IsNullOrEmpty(tn))
{
b.HasCheckConstraint(string.Concat("ck_", tn, "_tenant_not_null"), "tenant_id is not null");
b.HasCheckConstraint(string.Concat("ck_", tn, "_tenant_not_zero"),
"tenant_id <> '00000000-0000-0000-0000-000000000000'");
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'");
}
// Audit
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnName("created_at")
b.Property<DateTimeOffset>(nameof(BaseEntity.CreatedAt)).HasColumnName("created_at")
.HasDefaultValueSql("now() at time zone 'utc'");
b.Property<DateTimeOffset?>("UpdatedAt").HasColumnName("updated_at");
b.Property<string?>("CreatedBy").HasColumnName("created_by");
b.Property<string?>("UpdatedBy").HasColumnName("updated_by");
b.Property<bool>("IsDeleted").HasColumnName("is_deleted").HasDefaultValue(false);
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);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,348 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace AMREZ.EOP.Infrastructures.Migrations
{
/// <inheritdoc />
public partial class AddCustomer : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "addresses",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
Line1 = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
Line2 = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
Village = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
Soi = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
Road = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
Subdistrict = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
District = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
Province = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
PostalCode = table.Column<string>(type: "character varying(10)", maxLength: 10, nullable: true),
CountryCode = table.Column<string>(type: "character varying(2)", maxLength: 2, nullable: false),
Verified = table.Column<bool>(type: "boolean", nullable: false),
GeoLat = table.Column<double>(type: "double precision", nullable: true),
GeoLng = table.Column<double>(type: "double precision", nullable: true),
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"),
created_by = table.Column<string>(type: "text", nullable: true),
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
updated_by = table.Column<string>(type: "text", nullable: true),
is_deleted = table.Column<bool>(type: "boolean", nullable: false, defaultValue: false)
},
constraints: table =>
{
table.PrimaryKey("PK_addresses", x => x.Id);
table.UniqueConstraint("AK_addresses_tenant_id_Id", x => new { x.tenant_id, x.Id });
table.CheckConstraint("ck_addresses_tenant_not_null", "tenant_id is not null");
table.CheckConstraint("ck_addresses_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
});
migrationBuilder.CreateTable(
name: "customer_profiles",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
Type = table.Column<int>(type: "integer", nullable: false),
DisplayName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
Status = table.Column<int>(type: "integer", nullable: false),
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"),
created_by = table.Column<string>(type: "text", nullable: true),
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
updated_by = table.Column<string>(type: "text", nullable: true),
is_deleted = table.Column<bool>(type: "boolean", nullable: false, defaultValue: false)
},
constraints: table =>
{
table.PrimaryKey("PK_customer_profiles", x => x.Id);
table.UniqueConstraint("AK_customer_profiles_tenant_id_Id", x => new { x.tenant_id, x.Id });
table.CheckConstraint("ck_customer_profiles_tenant_not_null", "tenant_id is not null");
table.CheckConstraint("ck_customer_profiles_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
});
migrationBuilder.CreateTable(
name: "customer_addresses",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
CustomerProfileId = table.Column<Guid>(type: "uuid", nullable: false),
AddressId = table.Column<Guid>(type: "uuid", nullable: false),
Label = table.Column<int>(type: "integer", nullable: false),
IsDefault = table.Column<bool>(type: "boolean", nullable: false, defaultValue: false),
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"),
created_by = table.Column<string>(type: "text", nullable: true),
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
updated_by = table.Column<string>(type: "text", nullable: true),
is_deleted = table.Column<bool>(type: "boolean", nullable: false, defaultValue: false)
},
constraints: table =>
{
table.PrimaryKey("PK_customer_addresses", x => x.Id);
table.CheckConstraint("ck_customer_addresses_tenant_not_null", "tenant_id is not null");
table.CheckConstraint("ck_customer_addresses_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
table.ForeignKey(
name: "FK_customer_addresses_addresses_tenant_id_AddressId",
columns: x => new { x.tenant_id, x.AddressId },
principalTable: "addresses",
principalColumns: new[] { "tenant_id", "Id" },
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_customer_addresses_customer_profiles_tenant_id_CustomerProf~",
columns: x => new { x.tenant_id, x.CustomerProfileId },
principalTable: "customer_profiles",
principalColumns: new[] { "tenant_id", "Id" },
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "customer_contacts",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
CustomerProfileId = table.Column<Guid>(type: "uuid", nullable: false),
Type = table.Column<int>(type: "integer", nullable: false),
Value = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
IsPrimary = table.Column<bool>(type: "boolean", nullable: false, defaultValue: false),
IsVerified = table.Column<bool>(type: "boolean", nullable: false, defaultValue: false),
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"),
created_by = table.Column<string>(type: "text", nullable: true),
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
updated_by = table.Column<string>(type: "text", nullable: true),
is_deleted = table.Column<bool>(type: "boolean", nullable: false, defaultValue: false)
},
constraints: table =>
{
table.PrimaryKey("PK_customer_contacts", x => x.Id);
table.CheckConstraint("ck_customer_contacts_tenant_not_null", "tenant_id is not null");
table.CheckConstraint("ck_customer_contacts_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
table.ForeignKey(
name: "FK_customer_contacts_customer_profiles_tenant_id_CustomerProfi~",
columns: x => new { x.tenant_id, x.CustomerProfileId },
principalTable: "customer_profiles",
principalColumns: new[] { "tenant_id", "Id" },
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "customer_tags",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
CustomerProfileId = table.Column<Guid>(type: "uuid", nullable: false),
Tag = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"),
created_by = table.Column<string>(type: "text", nullable: true),
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
updated_by = table.Column<string>(type: "text", nullable: true),
is_deleted = table.Column<bool>(type: "boolean", nullable: false, defaultValue: false)
},
constraints: table =>
{
table.PrimaryKey("PK_customer_tags", x => x.Id);
table.CheckConstraint("ck_customer_tags_tenant_not_null", "tenant_id is not null");
table.CheckConstraint("ck_customer_tags_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
table.ForeignKey(
name: "FK_customer_tags_customer_profiles_tenant_id_CustomerProfileId",
columns: x => new { x.tenant_id, x.CustomerProfileId },
principalTable: "customer_profiles",
principalColumns: new[] { "tenant_id", "Id" },
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "organization_profiles",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
CustomerProfileId = table.Column<Guid>(type: "uuid", nullable: false),
LegalNameTh = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
LegalNameEn = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
RegistrationNo = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
TaxId13 = table.Column<string>(type: "character varying(13)", maxLength: 13, nullable: true),
BranchCode = table.Column<string>(type: "character varying(5)", maxLength: 5, nullable: true),
CompanyType = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"),
created_by = table.Column<string>(type: "text", nullable: true),
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
updated_by = table.Column<string>(type: "text", nullable: true),
is_deleted = table.Column<bool>(type: "boolean", nullable: false, defaultValue: false)
},
constraints: table =>
{
table.PrimaryKey("PK_organization_profiles", x => x.Id);
table.CheckConstraint("ck_organization_profiles_tenant_not_null", "tenant_id is not null");
table.CheckConstraint("ck_organization_profiles_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
table.ForeignKey(
name: "FK_organization_profiles_customer_profiles_tenant_id_CustomerP~",
columns: x => new { x.tenant_id, x.CustomerProfileId },
principalTable: "customer_profiles",
principalColumns: new[] { "tenant_id", "Id" },
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "person_profiles",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
CustomerProfileId = table.Column<Guid>(type: "uuid", nullable: false),
Prefix = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
FirstNameTh = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
LastNameTh = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
FirstNameEn = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
LastNameEn = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
BirthDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
NationalId = table.Column<string>(type: "character varying(13)", maxLength: 13, nullable: true),
PassportNo = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"),
created_by = table.Column<string>(type: "text", nullable: true),
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
updated_by = table.Column<string>(type: "text", nullable: true),
is_deleted = table.Column<bool>(type: "boolean", nullable: false, defaultValue: false)
},
constraints: table =>
{
table.PrimaryKey("PK_person_profiles", x => x.Id);
table.CheckConstraint("ck_person_profiles_tenant_not_null", "tenant_id is not null");
table.CheckConstraint("ck_person_profiles_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
table.ForeignKey(
name: "FK_person_profiles_customer_profiles_tenant_id_CustomerProfile~",
columns: x => new { x.tenant_id, x.CustomerProfileId },
principalTable: "customer_profiles",
principalColumns: new[] { "tenant_id", "Id" },
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_addresses_tenant_id",
table: "addresses",
column: "tenant_id");
migrationBuilder.CreateIndex(
name: "IX_customer_addresses_tenant_id",
table: "customer_addresses",
column: "tenant_id");
migrationBuilder.CreateIndex(
name: "IX_customer_addresses_tenant_id_AddressId",
table: "customer_addresses",
columns: new[] { "tenant_id", "AddressId" });
migrationBuilder.CreateIndex(
name: "IX_customer_addresses_tenant_id_CustomerProfileId_Label",
table: "customer_addresses",
columns: new[] { "tenant_id", "CustomerProfileId", "Label" });
migrationBuilder.CreateIndex(
name: "ux_default_customer_address_per_label",
table: "customer_addresses",
columns: new[] { "tenant_id", "CustomerProfileId", "Label", "IsDefault" },
unique: true,
filter: "\"IsDefault\" = TRUE");
migrationBuilder.CreateIndex(
name: "IX_customer_contacts_tenant_id",
table: "customer_contacts",
column: "tenant_id");
migrationBuilder.CreateIndex(
name: "IX_customer_contacts_tenant_id_CustomerProfileId_Type",
table: "customer_contacts",
columns: new[] { "tenant_id", "CustomerProfileId", "Type" });
migrationBuilder.CreateIndex(
name: "ux_customer_contact_primary_per_type",
table: "customer_contacts",
columns: new[] { "tenant_id", "CustomerProfileId", "Type", "IsPrimary" },
unique: true,
filter: "\"IsPrimary\" = TRUE");
migrationBuilder.CreateIndex(
name: "IX_customer_profiles_tenant_id",
table: "customer_profiles",
column: "tenant_id");
migrationBuilder.CreateIndex(
name: "IX_customer_profiles_tenant_id_DisplayName",
table: "customer_profiles",
columns: new[] { "tenant_id", "DisplayName" });
migrationBuilder.CreateIndex(
name: "IX_customer_tags_tenant_id",
table: "customer_tags",
column: "tenant_id");
migrationBuilder.CreateIndex(
name: "IX_customer_tags_tenant_id_CustomerProfileId_Tag",
table: "customer_tags",
columns: new[] { "tenant_id", "CustomerProfileId", "Tag" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_organization_profiles_tenant_id",
table: "organization_profiles",
column: "tenant_id");
migrationBuilder.CreateIndex(
name: "IX_organization_profiles_tenant_id_CustomerProfileId",
table: "organization_profiles",
columns: new[] { "tenant_id", "CustomerProfileId" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_organization_profiles_tenant_id_TaxId13_BranchCode",
table: "organization_profiles",
columns: new[] { "tenant_id", "TaxId13", "BranchCode" },
unique: true,
filter: "\"TaxId13\" IS NOT NULL");
migrationBuilder.CreateIndex(
name: "IX_person_profiles_tenant_id",
table: "person_profiles",
column: "tenant_id");
migrationBuilder.CreateIndex(
name: "IX_person_profiles_tenant_id_CustomerProfileId",
table: "person_profiles",
columns: new[] { "tenant_id", "CustomerProfileId" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "customer_addresses");
migrationBuilder.DropTable(
name: "customer_contacts");
migrationBuilder.DropTable(
name: "customer_tags");
migrationBuilder.DropTable(
name: "organization_profiles");
migrationBuilder.DropTable(
name: "person_profiles");
migrationBuilder.DropTable(
name: "addresses");
migrationBuilder.DropTable(
name: "customer_profiles");
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -65,47 +65,16 @@ public class UserRepository : IUserRepository
public async Task<User?> FindActiveByEmailAsync(string email, CancellationToken ct = default)
{
var r = _redis?.GetDatabase();
var norm = email.Trim().ToLowerInvariant();
if (r is not null)
{
try
{
var cached = await r.StringGetAsync($"uidx:{norm}");
if (cached.HasValue)
{
var hit = JsonSerializer.Deserialize<User>(cached!);
if (hit?.IsActive == true) return hit;
}
}
catch { /* ignore cache errors */ }
}
var db = _scope.Get<AppDbContext>();
var user = await (
from u in db.Users.AsNoTracking()
join i in db.UserIdentities.AsNoTracking() on u.Id equals i.UserId
where u.IsActive
&& i.Type == IdentityType.Email
&& i.Identifier == norm
select u
).FirstOrDefaultAsync(ct);
if (user is not null && r is not null)
{
try
{
var payload = JsonSerializer.Serialize(user);
var ttl = TimeSpan.FromMinutes(5);
await r.StringSetAsync($"uidx:{norm}", payload, ttl);
await r.StringSetAsync($"user:{user.Id:N}", payload, ttl);
}
catch { /* ignore cache errors */ }
}
return user;
return await db.Users
.AsNoTracking()
.Include(u => u.UserRoles)
.ThenInclude(ur => ur.Role)
.Where(u => u.IsActive &&
u.Identities.Any(i => i.Type == IdentityType.Email && i.Identifier == norm))
.FirstOrDefaultAsync(ct);
}
public async Task<bool> EmailExistsAsync(string email, CancellationToken ct = default)
@@ -142,11 +111,61 @@ public class UserRepository : IUserRepository
if (!string.IsNullOrWhiteSpace(email))
await r.StringSetAsync(IdentityEmailKey(tid.ToString(), email!), payload, ttl);
}
catch { }
catch
{
}
}
}
public async Task AddIdentityAsync(Guid userId, IdentityType type, string identifier, bool isPrimary, CancellationToken ct = default)
public async Task<string[]> GetRoleCodesByUserIdAsync(Guid userId, Guid tenantId, CancellationToken ct = default)
{
var r = _redis?.GetDatabase();
var cacheKey = $"urole:{tenantId:N}:{userId:N}";
// ---- cache hit ----
if (r is not null)
{
try
{
var cached = await r.StringGetAsync(cacheKey);
if (cached.HasValue)
{
var arr = JsonSerializer.Deserialize<string[]>(cached!) ?? Array.Empty<string>();
if (arr.Length > 0) return arr;
}
}
catch { /* ignore cache errors */ }
}
var db = _scope.Get<AppDbContext>();
// NOTE:
// - ไม่อ้างอิง r.TenantId เพื่อให้คอมไพล์ได้แม้ Role ยังไม่มีฟิลด์ TenantId
// - ถ้าคุณเพิ่ม Role.TenantId แล้ว และต้องการกรองตาม tenant ให้เติม .Where(role => role.TenantId == tenantId)
var roles = await db.UserRoles
.AsNoTracking()
.Where(ur => ur.UserId == userId)
.Select(ur => ur.Role)
.Where(role => role != null)
.Select(role => role!.Code) // ใช้ Code เป็นค่าของ claim "role"
.Distinct()
.ToArrayAsync(ct);
// ---- cache miss → set ----
if (r is not null)
{
try
{
await r.StringSetAsync(cacheKey, JsonSerializer.Serialize(roles), TimeSpan.FromMinutes(5));
}
catch { /* ignore cache errors */ }
}
return roles;
}
public async Task AddIdentityAsync(Guid userId, IdentityType type, string identifier, bool isPrimary,
CancellationToken ct = default)
{
var tid = TenantId();
var db = _scope.Get<AppDbContext>();
@@ -162,18 +181,19 @@ public class UserRepository : IUserRepository
var entity = new UserIdentity
{
TenantId = tid,
UserId = userId,
Type = type,
TenantId = tid,
UserId = userId,
Type = type,
Identifier = norm,
IsPrimary = isPrimary
IsPrimary = isPrimary
};
await db.UserIdentities.AddAsync(entity, ct);
await db.SaveChangesAsync(ct);
}
public async Task VerifyIdentityAsync(Guid userId, IdentityType type, string identifier, DateTimeOffset verifiedAt, CancellationToken ct = default)
public async Task VerifyIdentityAsync(Guid userId, IdentityType type, string identifier, DateTimeOffset verifiedAt,
CancellationToken ct = default)
{
var tid = TenantId();
var db = _scope.Get<AppDbContext>();
@@ -191,7 +211,8 @@ public class UserRepository : IUserRepository
await db.SaveChangesAsync(ct);
}
public async Task<UserIdentity?> GetPrimaryIdentityAsync(Guid userId, IdentityType type, CancellationToken ct = default)
public async Task<UserIdentity?> GetPrimaryIdentityAsync(Guid userId, IdentityType type,
CancellationToken ct = default)
{
var tid = TenantId();
var db = _scope.Get<AppDbContext>();
@@ -234,7 +255,8 @@ public class UserRepository : IUserRepository
await db.SaveChangesAsync(ct);
}
public async Task<UserMfaFactor> AddTotpFactorAsync(Guid userId, string label, string secret, CancellationToken ct = default)
public async Task<UserMfaFactor> AddTotpFactorAsync(Guid userId, string label, string secret,
CancellationToken ct = default)
{
var tid = TenantId();
var db = _scope.Get<AppDbContext>();
@@ -294,7 +316,8 @@ public class UserRepository : IUserRepository
return session;
}
public async Task<UserSession?> FindSessionByRefreshHashAsync(Guid tenantId, string refreshTokenHash, CancellationToken ct = default)
public async Task<UserSession?> FindSessionByRefreshHashAsync(Guid tenantId, string refreshTokenHash,
CancellationToken ct = default)
{
var db = _scope.Get<AppDbContext>();
return await db.UserSessions
@@ -302,7 +325,8 @@ public class UserRepository : IUserRepository
.FirstOrDefaultAsync(x => x.TenantId == tenantId && x.RefreshTokenHash == refreshTokenHash, ct);
}
public async Task<bool> RotateSessionRefreshAsync(Guid tenantId, Guid sessionId, string newRefreshTokenHash, DateTimeOffset newIssuedAt, DateTimeOffset? newExpiresAt, CancellationToken ct = default)
public async Task<bool> RotateSessionRefreshAsync(Guid tenantId, Guid sessionId, string newRefreshTokenHash,
DateTimeOffset newIssuedAt, DateTimeOffset? newExpiresAt, CancellationToken ct = default)
{
var db = _scope.Get<AppDbContext>();
var s = await db.UserSessions.FirstOrDefaultAsync(x => x.TenantId == tenantId && x.Id == sessionId, ct);
@@ -319,7 +343,8 @@ public class UserRepository : IUserRepository
var tid = TenantId();
var db = _scope.Get<AppDbContext>();
var s = await db.UserSessions.FirstOrDefaultAsync(x => x.TenantId == tid && x.Id == sessionId && x.UserId == userId, ct);
var s = await db.UserSessions.FirstOrDefaultAsync(
x => x.TenantId == tid && x.Id == sessionId && x.UserId == userId, ct);
if (s is null) return 0;
s.RevokedAt = DateTimeOffset.UtcNow;