Add Master Data
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
using System.Security.Claims;
|
||||
using AMREZ.EOP.Abstractions.Applications.UseCases.Authentications;
|
||||
using AMREZ.EOP.Application.UseCases.Authentications;
|
||||
using AMREZ.EOP.Contracts.DTOs.Authentications.AddEmailIdentity;
|
||||
using AMREZ.EOP.Contracts.DTOs.Authentications.AssignRole;
|
||||
using AMREZ.EOP.Contracts.DTOs.Authentications.ChangePassword;
|
||||
using AMREZ.EOP.Contracts.DTOs.Authentications.DisableMfa;
|
||||
using AMREZ.EOP.Contracts.DTOs.Authentications.EnableTotp;
|
||||
@@ -10,6 +12,8 @@ using AMREZ.EOP.Contracts.DTOs.Authentications.Logout;
|
||||
using AMREZ.EOP.Contracts.DTOs.Authentications.LogoutAll;
|
||||
using AMREZ.EOP.Contracts.DTOs.Authentications.Refresh;
|
||||
using AMREZ.EOP.Contracts.DTOs.Authentications.Register;
|
||||
using AMREZ.EOP.Contracts.DTOs.Authentications.Role;
|
||||
using AMREZ.EOP.Contracts.DTOs.Authentications.UnassignRole;
|
||||
using AMREZ.EOP.Contracts.DTOs.Authentications.VerifyEmail;
|
||||
using AMREZ.EOP.Domain.Shared.Contracts;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
@@ -37,6 +41,7 @@ public class AuthenticationController : ControllerBase
|
||||
private readonly IIssueTokenPairUseCase _issueTokens;
|
||||
private readonly IRefreshUseCase _refresh;
|
||||
|
||||
|
||||
public AuthenticationController(
|
||||
ILoginUseCase login,
|
||||
IRegisterUseCase register,
|
||||
@@ -74,9 +79,21 @@ public class AuthenticationController : ControllerBase
|
||||
new(ClaimTypes.NameIdentifier, res.UserId.ToString()),
|
||||
new(ClaimTypes.Name, res.Email),
|
||||
new(ClaimTypes.Email, res.Email),
|
||||
new("tenant", res.TenantKey)
|
||||
new("tenant", res.TenantKey),
|
||||
new("tenantId", res.TenantId.ToString())
|
||||
};
|
||||
|
||||
var roles = (res.Roles ?? Array.Empty<string>())
|
||||
.Where(r => !string.IsNullOrWhiteSpace(r))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
foreach (var r in roles)
|
||||
claims.Add(new Claim(ClaimTypes.Role, r));
|
||||
|
||||
if (roles.Length > 0)
|
||||
claims.Add(new Claim("roles_csv", string.Join(",", roles)));
|
||||
|
||||
var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, AuthPolicies.Scheme));
|
||||
await HttpContext.SignInAsync(AuthPolicies.Scheme, principal);
|
||||
|
||||
@@ -85,7 +102,8 @@ public class AuthenticationController : ControllerBase
|
||||
UserId = res.UserId,
|
||||
TenantId = res.TenantId,
|
||||
Tenant = res.TenantKey,
|
||||
Email = res.Email
|
||||
Email = res.Email,
|
||||
Roles = roles
|
||||
}, ct);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(tokenPair.RefreshToken))
|
||||
@@ -105,6 +123,7 @@ public class AuthenticationController : ControllerBase
|
||||
return Ok(new
|
||||
{
|
||||
user = res,
|
||||
roles,
|
||||
access_token = tokenPair.AccessToken,
|
||||
token_type = "Bearer",
|
||||
expires_at = tokenPair.AccessExpiresAt
|
||||
@@ -164,6 +183,8 @@ public class AuthenticationController : ControllerBase
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
|
||||
|
||||
[HttpPost("logout")]
|
||||
public async Task<IActionResult> Logout()
|
||||
{
|
||||
|
||||
@@ -10,6 +10,7 @@ public interface IUserRepository
|
||||
Task<User?> FindActiveByEmailAsync(string email, CancellationToken ct = default);
|
||||
Task<bool> EmailExistsAsync(string email, CancellationToken ct = default);
|
||||
Task AddAsync(User user, CancellationToken ct = default);
|
||||
Task<string[]> GetRoleCodesByUserIdAsync(Guid userId, Guid tenantId, CancellationToken ct = default);
|
||||
|
||||
Task AddIdentityAsync(Guid userId, IdentityType type, string identifier, bool isPrimary, CancellationToken ct = default);
|
||||
Task VerifyIdentityAsync(Guid userId, IdentityType type, string identifier, DateTimeOffset verifiedAt, CancellationToken ct = default);
|
||||
|
||||
@@ -37,6 +37,7 @@ public sealed class IssueTokenPairUseCase : IIssueTokenPairUseCase
|
||||
|
||||
var tenantId = request.TenantId;
|
||||
|
||||
// ---- สร้าง/บันทึก refresh session ----
|
||||
var refreshRaw = Convert.ToBase64String(RandomNumberGenerator.GetBytes(64));
|
||||
var refreshHash = Sha256(refreshRaw);
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
@@ -54,26 +55,44 @@ public sealed class IssueTokenPairUseCase : IIssueTokenPairUseCase
|
||||
IpAddress = http.Connection.RemoteIpAddress?.ToString()
|
||||
}, ct);
|
||||
|
||||
// ---- เวอร์ชัน/สแตมป์ความปลอดภัย ----
|
||||
var tv = await _users.GetTenantTokenVersionAsync(tenantId, ct);
|
||||
var sstamp = await _users.GetUserSecurityStampAsync(request.UserId, ct) ?? string.Empty;
|
||||
|
||||
// ---- เตรียม claims พื้นฐาน ----
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new("sub", request.UserId.ToString()),
|
||||
new("email", request.Email),
|
||||
new("tenant", request.Tenant), // tenantKey ที่ FE ใช้
|
||||
new("tenant_id", tenantId.ToString()),
|
||||
new("sid", session.Id.ToString()),
|
||||
new("jti", Guid.NewGuid().ToString("N")),
|
||||
new("tv", tv),
|
||||
new("sstamp", sstamp)
|
||||
new("sstamp", sstamp),
|
||||
new("iat", now.ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64)
|
||||
};
|
||||
|
||||
string[] roles = request.Roles ?? Array.Empty<string>();
|
||||
if (roles.Length == 0)
|
||||
{
|
||||
roles = await _users.GetRoleCodesByUserIdAsync(request.UserId, tenantId, ct);
|
||||
}
|
||||
|
||||
foreach (var r in roles.Where(s => !string.IsNullOrWhiteSpace(s)).Distinct(StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
claims.Add(new Claim("role", r));
|
||||
claims.Add(new Claim(ClaimTypes.Role, r));
|
||||
}
|
||||
|
||||
// ---- ออก access token ----
|
||||
var (access, accessExp) = _jwt.CreateAccessToken(claims);
|
||||
|
||||
return new IssueTokenPairResponse
|
||||
{
|
||||
AccessToken = access,
|
||||
AccessExpiresAt = accessExp,
|
||||
RefreshToken = refreshRaw, // ส่ง raw กลับให้ client
|
||||
RefreshToken = refreshRaw,
|
||||
RefreshExpiresAt = session.ExpiresAt
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using AMREZ.EOP.Abstractions.Infrastructures.Common;
|
||||
using AMREZ.EOP.Abstractions.Infrastructures.Repositories;
|
||||
using AMREZ.EOP.Abstractions.Security;
|
||||
using AMREZ.EOP.Contracts.DTOs.Authentications.Login;
|
||||
using AMREZ.EOP.Domain.Entities.Authentications;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace AMREZ.EOP.Application.UseCases.Authentications
|
||||
@@ -60,9 +61,15 @@ namespace AMREZ.EOP.Application.UseCases.Authentications
|
||||
return null;
|
||||
}
|
||||
|
||||
var roles = (user.UserRoles ?? Enumerable.Empty<UserRole>())
|
||||
.Where(ur => ur.Role != null /*&& ur.Role.TenantId == user.TenantId*/) // ถ้า Role เป็น per-tenant ค่อยกรอง
|
||||
.Select(ur => ur.Role!.Code)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
await _uow.CommitAsync(ct);
|
||||
|
||||
return new LoginResponse(user.Id, user.TenantId, email, tenantKey);
|
||||
return new LoginResponse(user.Id, user.TenantId, email, tenantKey, roles);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace AMREZ.EOP.Contracts.DTOs.Authentications.AssignRole;
|
||||
|
||||
public sealed class AssignRoleRequest(Guid UserId, Guid TenantId, string RoleCode);
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace AMREZ.EOP.Contracts.DTOs.Authentications.AssignRole;
|
||||
|
||||
public sealed class AssignRoleResponse(string[] Roles);
|
||||
@@ -6,4 +6,5 @@ public sealed class IssueTokenPairRequest
|
||||
public Guid TenantId { get; init; } = default!;
|
||||
public string Tenant { get; init; } = default!;
|
||||
public string Email { get; init; } = default!;
|
||||
public string[]? Roles { get; init; } = default;
|
||||
}
|
||||
@@ -4,5 +4,6 @@ public sealed record LoginResponse(
|
||||
Guid UserId,
|
||||
Guid TenantId,
|
||||
string Email,
|
||||
string TenantKey
|
||||
string TenantKey,
|
||||
string[] Roles
|
||||
);
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace AMREZ.EOP.Contracts.DTOs.Authentications.Role;
|
||||
|
||||
public sealed record RoleChange(string RoleCode);
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace AMREZ.EOP.Contracts.DTOs.Authentications.UnassignRole;
|
||||
|
||||
public sealed class UnassignRoleRequest(Guid UserId, Guid TenantId, string RoleCode);
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace AMREZ.EOP.Contracts.DTOs.Authentications.UnassignRole;
|
||||
|
||||
public sealed class UnassignRoleResponse(string[] Roles);
|
||||
@@ -4,7 +4,7 @@ namespace AMREZ.EOP.Domain.Entities.Authentications;
|
||||
|
||||
public sealed class Role : BaseEntity
|
||||
{
|
||||
public string Code { get; set; } = default!; // system code, unique per tenant
|
||||
public string Code { get; set; } = default!;
|
||||
public string Name { get; set; } = default!;
|
||||
|
||||
public ICollection<UserRole> UserRoles { get; set; } = new List<UserRole>();
|
||||
|
||||
25
AMREZ.EOP.Domain/Entities/Common/MasterBase.cs
Normal file
25
AMREZ.EOP.Domain/Entities/Common/MasterBase.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using AMREZ.EOP.Domain.Entities.Common;
|
||||
|
||||
namespace AMREZ.EOP.Domain.Entities.MasterData;
|
||||
|
||||
public abstract class MasterBase : BaseEntity
|
||||
{
|
||||
|
||||
public string Scope { get; set; } = "global";
|
||||
|
||||
public string Code { get; set; } = null!;
|
||||
public string Name { get; set; } = null!;
|
||||
public Dictionary<string, string>? NameI18n { get; set; }
|
||||
|
||||
public Guid? OverridesGlobalId { get; set; }
|
||||
|
||||
public bool IsActive { get; set; } = true;
|
||||
public bool IsSystem { get; set; } = false;
|
||||
|
||||
public Dictionary<string, object>? Meta { get; set; }
|
||||
}
|
||||
|
||||
public abstract class MasterBlockBase : BaseEntity
|
||||
{
|
||||
public Guid GlobalId { get; set; }
|
||||
}
|
||||
22
AMREZ.EOP.Domain/Entities/Customers/Address.cs
Normal file
22
AMREZ.EOP.Domain/Entities/Customers/Address.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using AMREZ.EOP.Domain.Entities.Common;
|
||||
|
||||
namespace AMREZ.EOP.Domain.Entities.Customers;
|
||||
|
||||
public sealed class Address : BaseEntity
|
||||
{
|
||||
public string? Line1 { get; set; }
|
||||
public string? Line2 { get; set; }
|
||||
public string? Village { get; set; }
|
||||
public string? Soi { get; set; }
|
||||
public string? Road { get; set; }
|
||||
public string? Subdistrict { get; set; }
|
||||
public string? District { get; set; }
|
||||
public string? Province { get; set; }
|
||||
public string? PostalCode { get; set; }
|
||||
public string CountryCode { get; set; } = "TH";
|
||||
public bool Verified { get; set; }
|
||||
public double? GeoLat { get; set; }
|
||||
public double? GeoLng { get; set; }
|
||||
|
||||
public ICollection<CustomerAddress> CustomerLinks { get; set; } = new List<CustomerAddress>();
|
||||
}
|
||||
15
AMREZ.EOP.Domain/Entities/Customers/CustomerAddress.cs
Normal file
15
AMREZ.EOP.Domain/Entities/Customers/CustomerAddress.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using AMREZ.EOP.Domain.Entities.Common;
|
||||
using AMREZ.EOP.Domain.Shared.Customer;
|
||||
|
||||
namespace AMREZ.EOP.Domain.Entities.Customers;
|
||||
|
||||
public sealed class CustomerAddress : BaseEntity
|
||||
{
|
||||
public Guid CustomerProfileId { get; set; }
|
||||
public Guid AddressId { get; set; }
|
||||
public AddressLabel Label { get; set; }
|
||||
public bool IsDefault { get; set; }
|
||||
|
||||
public CustomerProfile Customer { get; set; } = default!;
|
||||
public Address Address { get; set; } = default!;
|
||||
}
|
||||
15
AMREZ.EOP.Domain/Entities/Customers/CustomerContact.cs
Normal file
15
AMREZ.EOP.Domain/Entities/Customers/CustomerContact.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using AMREZ.EOP.Domain.Entities.Common;
|
||||
using AMREZ.EOP.Domain.Shared.Customer;
|
||||
|
||||
namespace AMREZ.EOP.Domain.Entities.Customers;
|
||||
|
||||
public sealed class CustomerContact : BaseEntity
|
||||
{
|
||||
public Guid CustomerProfileId { get; set; }
|
||||
public ContactType Type { get; set; }
|
||||
public string Value { get; set; } = default!;
|
||||
public bool IsPrimary { get; set; }
|
||||
public bool IsVerified { get; set; }
|
||||
|
||||
public CustomerProfile Customer { get; set; } = default!;
|
||||
}
|
||||
18
AMREZ.EOP.Domain/Entities/Customers/CustomerProfile.cs
Normal file
18
AMREZ.EOP.Domain/Entities/Customers/CustomerProfile.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using AMREZ.EOP.Domain.Entities.Common;
|
||||
using AMREZ.EOP.Domain.Shared.Customer;
|
||||
|
||||
namespace AMREZ.EOP.Domain.Entities.Customers;
|
||||
|
||||
public sealed class CustomerProfile : BaseEntity
|
||||
{
|
||||
public PartyType Type { get; set; }
|
||||
public string DisplayName { get; set; } = default!;
|
||||
public CustomerStatus Status { get; set; } = CustomerStatus.Active;
|
||||
|
||||
public PersonProfile? Person { get; set; }
|
||||
public OrganizationProfile? Organization { get; set; }
|
||||
|
||||
public ICollection<CustomerContact> Contacts { get; set; } = new List<CustomerContact>();
|
||||
public ICollection<CustomerAddress> Addresses { get; set; } = new List<CustomerAddress>();
|
||||
public ICollection<CustomerTag> Tags { get; set; } = new List<CustomerTag>();
|
||||
}
|
||||
11
AMREZ.EOP.Domain/Entities/Customers/CustomerTag.cs
Normal file
11
AMREZ.EOP.Domain/Entities/Customers/CustomerTag.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using AMREZ.EOP.Domain.Entities.Common;
|
||||
|
||||
namespace AMREZ.EOP.Domain.Entities.Customers;
|
||||
|
||||
public sealed class CustomerTag : BaseEntity
|
||||
{
|
||||
public Guid CustomerProfileId { get; set; }
|
||||
public string Tag { get; set; } = default!;
|
||||
|
||||
public CustomerProfile Customer { get; set; } = default!;
|
||||
}
|
||||
17
AMREZ.EOP.Domain/Entities/Customers/OrganizationProfile.cs
Normal file
17
AMREZ.EOP.Domain/Entities/Customers/OrganizationProfile.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using AMREZ.EOP.Domain.Entities.Common;
|
||||
|
||||
namespace AMREZ.EOP.Domain.Entities.Customers;
|
||||
|
||||
public sealed class OrganizationProfile : BaseEntity
|
||||
{
|
||||
public Guid CustomerProfileId { get; set; }
|
||||
|
||||
public string LegalNameTh { get; set; } = default!;
|
||||
public string? LegalNameEn { get; set; }
|
||||
public string? RegistrationNo { get; set; }
|
||||
public string? TaxId13 { get; set; }
|
||||
public string? BranchCode { get; set; }
|
||||
public string? CompanyType { get; set; }
|
||||
|
||||
public CustomerProfile Customer { get; set; } = default!;
|
||||
}
|
||||
19
AMREZ.EOP.Domain/Entities/Customers/PersonProfile.cs
Normal file
19
AMREZ.EOP.Domain/Entities/Customers/PersonProfile.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using AMREZ.EOP.Domain.Entities.Common;
|
||||
|
||||
namespace AMREZ.EOP.Domain.Entities.Customers;
|
||||
|
||||
public sealed class PersonProfile : BaseEntity
|
||||
{
|
||||
public Guid CustomerProfileId { get; set; }
|
||||
|
||||
public string? Prefix { get; set; }
|
||||
public string? FirstNameTh { get; set; }
|
||||
public string? LastNameTh { get; set; }
|
||||
public string? FirstNameEn { get; set; }
|
||||
public string? LastNameEn { get; set; }
|
||||
public DateTime? BirthDate { get; set; }
|
||||
public string? NationalId { get; set; }
|
||||
public string? PassportNo { get; set; }
|
||||
|
||||
public CustomerProfile Customer { get; set; } = default!;
|
||||
}
|
||||
9
AMREZ.EOP.Domain/Entities/MasterData/Allergen.cs
Normal file
9
AMREZ.EOP.Domain/Entities/MasterData/Allergen.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace AMREZ.EOP.Domain.Entities.MasterData;
|
||||
|
||||
public class Allergen : MasterBase
|
||||
{
|
||||
}
|
||||
|
||||
public class AllergenBlock : MasterBlockBase
|
||||
{
|
||||
}
|
||||
10
AMREZ.EOP.Domain/Entities/MasterData/Brand.cs
Normal file
10
AMREZ.EOP.Domain/Entities/MasterData/Brand.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace AMREZ.EOP.Domain.Entities.MasterData;
|
||||
|
||||
public class Brand : MasterBase
|
||||
{
|
||||
public string? ExternalRef { get; set; }
|
||||
}
|
||||
|
||||
public class BrandBlock : MasterBlockBase
|
||||
{
|
||||
}
|
||||
26
AMREZ.EOP.Domain/Entities/MasterData/Category.cs
Normal file
26
AMREZ.EOP.Domain/Entities/MasterData/Category.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
namespace AMREZ.EOP.Domain.Entities.MasterData;
|
||||
|
||||
public class Category : MasterBase
|
||||
{
|
||||
public Guid? ParentId { get; set; }
|
||||
public Category? Parent { get; set; }
|
||||
public ICollection<Category> Children { get; set; } = new List<Category>();
|
||||
public string? Path { get; set; }
|
||||
public int SortOrder { get; set; } = 0;
|
||||
}
|
||||
|
||||
public class CategoryBlock : MasterBlockBase
|
||||
{
|
||||
}
|
||||
|
||||
public class CategoryExt
|
||||
{
|
||||
public Guid CategoryId { get; set; }
|
||||
public Category Category { get; set; } = null!;
|
||||
public string? Slug { get; set; }
|
||||
public string? SeoTitle { get; set; }
|
||||
public string? SeoDescription { get; set; }
|
||||
public string? ImageUrl { get; set; }
|
||||
public Dictionary<string, object>? ChannelVisibility { get; set; }
|
||||
public Dictionary<string, object>? FacetSchema { get; set; }
|
||||
}
|
||||
9
AMREZ.EOP.Domain/Entities/MasterData/ComplianceStatus.cs
Normal file
9
AMREZ.EOP.Domain/Entities/MasterData/ComplianceStatus.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace AMREZ.EOP.Domain.Entities.MasterData;
|
||||
|
||||
public class ComplianceStatus : MasterBase
|
||||
{
|
||||
}
|
||||
|
||||
public class ComplianceStatusBlock : MasterBlockBase
|
||||
{
|
||||
}
|
||||
9
AMREZ.EOP.Domain/Entities/MasterData/Country.cs
Normal file
9
AMREZ.EOP.Domain/Entities/MasterData/Country.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace AMREZ.EOP.Domain.Entities.MasterData;
|
||||
|
||||
public class Country : MasterBase
|
||||
{
|
||||
}
|
||||
|
||||
public class CountryBlock : MasterBlockBase
|
||||
{
|
||||
}
|
||||
10
AMREZ.EOP.Domain/Entities/MasterData/Currency.cs
Normal file
10
AMREZ.EOP.Domain/Entities/MasterData/Currency.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace AMREZ.EOP.Domain.Entities.MasterData;
|
||||
|
||||
public class Currency : MasterBase
|
||||
{
|
||||
public byte Exponent { get; set; } = 2;
|
||||
}
|
||||
|
||||
public class CurrencyBlock : MasterBlockBase
|
||||
{
|
||||
}
|
||||
9
AMREZ.EOP.Domain/Entities/MasterData/DocControlStatus.cs
Normal file
9
AMREZ.EOP.Domain/Entities/MasterData/DocControlStatus.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace AMREZ.EOP.Domain.Entities.MasterData;
|
||||
|
||||
public class DocControlStatus : MasterBase
|
||||
{
|
||||
}
|
||||
|
||||
public class DocControlStatusBlock : MasterBlockBase
|
||||
{
|
||||
}
|
||||
9
AMREZ.EOP.Domain/Entities/MasterData/FuncTest.cs
Normal file
9
AMREZ.EOP.Domain/Entities/MasterData/FuncTest.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace AMREZ.EOP.Domain.Entities.MasterData;
|
||||
|
||||
public class FuncTest : MasterBase
|
||||
{
|
||||
}
|
||||
|
||||
public class FuncTestBlock : MasterBlockBase
|
||||
{
|
||||
}
|
||||
9
AMREZ.EOP.Domain/Entities/MasterData/HazardClass.cs
Normal file
9
AMREZ.EOP.Domain/Entities/MasterData/HazardClass.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace AMREZ.EOP.Domain.Entities.MasterData;
|
||||
|
||||
public class HazardClass : MasterBase
|
||||
{
|
||||
}
|
||||
|
||||
public class HazardClassBlock : MasterBlockBase
|
||||
{
|
||||
}
|
||||
9
AMREZ.EOP.Domain/Entities/MasterData/Language.cs
Normal file
9
AMREZ.EOP.Domain/Entities/MasterData/Language.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace AMREZ.EOP.Domain.Entities.MasterData;
|
||||
|
||||
public class Language : MasterBase
|
||||
{
|
||||
}
|
||||
|
||||
public class LanguageBlock : MasterBlockBase
|
||||
{
|
||||
}
|
||||
10
AMREZ.EOP.Domain/Entities/MasterData/Manufacturer.cs
Normal file
10
AMREZ.EOP.Domain/Entities/MasterData/Manufacturer.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace AMREZ.EOP.Domain.Entities.MasterData;
|
||||
|
||||
public class Manufacturer : MasterBase
|
||||
{
|
||||
public string? CountryCode { get; set; }
|
||||
}
|
||||
|
||||
public class ManufacturerBlock : MasterBlockBase
|
||||
{
|
||||
}
|
||||
9
AMREZ.EOP.Domain/Entities/MasterData/Market.cs
Normal file
9
AMREZ.EOP.Domain/Entities/MasterData/Market.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace AMREZ.EOP.Domain.Entities.MasterData;
|
||||
|
||||
public class Market : MasterBase
|
||||
{
|
||||
}
|
||||
|
||||
public class MarketBlock : MasterBlockBase
|
||||
{
|
||||
}
|
||||
9
AMREZ.EOP.Domain/Entities/MasterData/PackingGroup.cs
Normal file
9
AMREZ.EOP.Domain/Entities/MasterData/PackingGroup.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace AMREZ.EOP.Domain.Entities.MasterData;
|
||||
|
||||
public class PackingGroup : MasterBase
|
||||
{
|
||||
}
|
||||
|
||||
public class PackingGroupBlock : MasterBlockBase
|
||||
{
|
||||
}
|
||||
9
AMREZ.EOP.Domain/Entities/MasterData/QaStage.cs
Normal file
9
AMREZ.EOP.Domain/Entities/MasterData/QaStage.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace AMREZ.EOP.Domain.Entities.MasterData;
|
||||
|
||||
public class QaStage : MasterBase
|
||||
{
|
||||
}
|
||||
|
||||
public class QaStageBlock : MasterBlockBase
|
||||
{
|
||||
}
|
||||
9
AMREZ.EOP.Domain/Entities/MasterData/QcStatus.cs
Normal file
9
AMREZ.EOP.Domain/Entities/MasterData/QcStatus.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace AMREZ.EOP.Domain.Entities.MasterData;
|
||||
|
||||
public class QcStatus : MasterBase
|
||||
{
|
||||
}
|
||||
|
||||
public class QcStatusBlock : MasterBlockBase
|
||||
{
|
||||
}
|
||||
9
AMREZ.EOP.Domain/Entities/MasterData/RecallClass.cs
Normal file
9
AMREZ.EOP.Domain/Entities/MasterData/RecallClass.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace AMREZ.EOP.Domain.Entities.MasterData;
|
||||
|
||||
public class RecallClass : MasterBase
|
||||
{
|
||||
}
|
||||
|
||||
public class RecallClassBlock : MasterBlockBase
|
||||
{
|
||||
}
|
||||
9
AMREZ.EOP.Domain/Entities/MasterData/RiskClass.cs
Normal file
9
AMREZ.EOP.Domain/Entities/MasterData/RiskClass.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace AMREZ.EOP.Domain.Entities.MasterData;
|
||||
|
||||
public class RiskClass : MasterBase
|
||||
{
|
||||
}
|
||||
|
||||
public class RiskClassBlock : MasterBlockBase
|
||||
{
|
||||
}
|
||||
9
AMREZ.EOP.Domain/Entities/MasterData/Route.cs
Normal file
9
AMREZ.EOP.Domain/Entities/MasterData/Route.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace AMREZ.EOP.Domain.Entities.MasterData;
|
||||
|
||||
public class Route : MasterBase
|
||||
{
|
||||
}
|
||||
|
||||
public class RouteBlock : MasterBlockBase
|
||||
{
|
||||
}
|
||||
9
AMREZ.EOP.Domain/Entities/MasterData/RxSchedule.cs
Normal file
9
AMREZ.EOP.Domain/Entities/MasterData/RxSchedule.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace AMREZ.EOP.Domain.Entities.MasterData;
|
||||
|
||||
public class RxSchedule : MasterBase
|
||||
{
|
||||
}
|
||||
|
||||
public class RxScheduleBlock : MasterBlockBase
|
||||
{
|
||||
}
|
||||
9
AMREZ.EOP.Domain/Entities/MasterData/Species.cs
Normal file
9
AMREZ.EOP.Domain/Entities/MasterData/Species.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace AMREZ.EOP.Domain.Entities.MasterData;
|
||||
|
||||
public class Species : MasterBase
|
||||
{
|
||||
}
|
||||
|
||||
public class SpeciesBlock : MasterBlockBase
|
||||
{
|
||||
}
|
||||
9
AMREZ.EOP.Domain/Entities/MasterData/StabilityStatus.cs
Normal file
9
AMREZ.EOP.Domain/Entities/MasterData/StabilityStatus.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace AMREZ.EOP.Domain.Entities.MasterData;
|
||||
|
||||
public class StabilityStatus : MasterBase
|
||||
{
|
||||
}
|
||||
|
||||
public class StabilityStatusBlock : MasterBlockBase
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace AMREZ.EOP.Domain.Entities.MasterData;
|
||||
|
||||
public class SterilizationMethod : MasterBase
|
||||
{
|
||||
}
|
||||
|
||||
public class SterilizationMethodBlock : MasterBlockBase
|
||||
{
|
||||
}
|
||||
11
AMREZ.EOP.Domain/Entities/MasterData/Uom.cs
Normal file
11
AMREZ.EOP.Domain/Entities/MasterData/Uom.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace AMREZ.EOP.Domain.Entities.MasterData;
|
||||
|
||||
public class Uom : MasterBase
|
||||
{
|
||||
public string? BaseCode { get; set; }
|
||||
public decimal? SiFactor { get; set; }
|
||||
}
|
||||
|
||||
public class UomBlock : MasterBlockBase
|
||||
{
|
||||
}
|
||||
9
AMREZ.EOP.Domain/Entities/MasterData/Vvm.cs
Normal file
9
AMREZ.EOP.Domain/Entities/MasterData/Vvm.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace AMREZ.EOP.Domain.Entities.MasterData;
|
||||
|
||||
public class Vvm : MasterBase
|
||||
{
|
||||
}
|
||||
|
||||
public class VvmBlock : MasterBlockBase
|
||||
{
|
||||
}
|
||||
9
AMREZ.EOP.Domain/Shared/Customer/AddressLabel.cs
Normal file
9
AMREZ.EOP.Domain/Shared/Customer/AddressLabel.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace AMREZ.EOP.Domain.Shared.Customer;
|
||||
|
||||
public enum AddressLabel
|
||||
{
|
||||
Billing,
|
||||
Shipping,
|
||||
Registered,
|
||||
Other
|
||||
}
|
||||
11
AMREZ.EOP.Domain/Shared/Customer/ContactType.cs
Normal file
11
AMREZ.EOP.Domain/Shared/Customer/ContactType.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace AMREZ.EOP.Domain.Shared.Customer;
|
||||
|
||||
public enum ContactType
|
||||
{
|
||||
Email,
|
||||
Phone,
|
||||
Line,
|
||||
Whatsapp,
|
||||
WeChat,
|
||||
Other
|
||||
}
|
||||
8
AMREZ.EOP.Domain/Shared/Customer/CustomerStatus.cs
Normal file
8
AMREZ.EOP.Domain/Shared/Customer/CustomerStatus.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace AMREZ.EOP.Domain.Shared.Customer;
|
||||
|
||||
public enum CustomerStatus
|
||||
{
|
||||
Active,
|
||||
Inactive,
|
||||
Blacklisted
|
||||
}
|
||||
7
AMREZ.EOP.Domain/Shared/Customer/PartyType.cs
Normal file
7
AMREZ.EOP.Domain/Shared/Customer/PartyType.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace AMREZ.EOP.Domain.Shared.Customer;
|
||||
|
||||
public enum PartyType
|
||||
{
|
||||
Person,
|
||||
Organization
|
||||
}
|
||||
@@ -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();
|
||||
@@ -107,7 +266,6 @@ public class AppDbContext : DbContext
|
||||
b.HasIndex(x => new { x.TenantId, x.UserId, x.Type, x.IsPrimary })
|
||||
.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 })
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -373,20 +531,272 @@ public class AppDbContext : DbContext
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
2176
AMREZ.EOP.Infrastructures/Migrations/20251009040810_AddCustomer.Designer.cs
generated
Normal file
2176
AMREZ.EOP.Infrastructures/Migrations/20251009040810_AddCustomer.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
5789
AMREZ.EOP.Infrastructures/Migrations/20251010092049_AddProductMasterData.Designer.cs
generated
Normal file
5789
AMREZ.EOP.Infrastructures/Migrations/20251010092049_AddProductMasterData.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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>();
|
||||
@@ -173,7 +192,8 @@ public class UserRepository : IUserRepository
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user