using AMREZ.EOP.Abstractions.Applications.Tenancy; using AMREZ.EOP.Abstractions.Infrastructures.Repositories; using AMREZ.EOP.Abstractions.Storage; using AMREZ.EOP.Domain.Entities.Tenancy; using AMREZ.EOP.Infrastructures.Data; using AMREZ.EOP.Infrastructures.Options; using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; namespace AMREZ.EOP.Infrastructures.Repositories; public sealed class TenantRepository : ITenantRepository { private readonly IDbScope _scope; private readonly ITenantResolver _resolver; private readonly IHttpContextAccessor _http; public TenantRepository(IDbScope scope, ITenantResolver resolver, IHttpContextAccessor http) { _scope = scope; _resolver = resolver; _http = http; } private AppDbContext Db() { var http = _http.HttpContext ?? throw new InvalidOperationException("No HttpContext"); var platform = _resolver.Resolve(http, "@platform") ?? throw new InvalidOperationException("No platform tenant"); _scope.EnsureForTenant(platform); return _scope.Get(); } private static string Norm(string s) => (s ?? string.Empty).Trim().ToLowerInvariant(); public Task TenantExistsAsync(string tenantKey, CancellationToken ct = default) { var key = Norm(tenantKey); return Db().Set().AsNoTracking().AnyAsync(x => x.TenantKey == key, ct); } public Task GetAsync(string tenantKey, CancellationToken ct = default) { var key = Norm(tenantKey); return Db().Set().AsNoTracking().FirstOrDefaultAsync(x => x.TenantKey == key, ct); } public async Task> ListTenantsAsync(CancellationToken ct = default) => await Db().Set().AsNoTracking().OrderBy(x => x.TenantKey).ToListAsync(ct); public async Task CreateAsync(TenantConfig row, CancellationToken ct = default) { row.TenantKey = Norm(row.TenantKey); row.Schema = string.IsNullOrWhiteSpace(row.Schema) ? null : row.Schema!.Trim(); row.ConnectionString = string.IsNullOrWhiteSpace(row.ConnectionString) ? null : row.ConnectionString!.Trim(); row.UpdatedAtUtc = DateTimeOffset.UtcNow; await Db().Set().AddAsync(row, ct); return await Db().SaveChangesAsync(ct) > 0; } public async Task UpdateAsync(TenantConfig row, DateTimeOffset? ifUnmodifiedSince, CancellationToken ct = default) { var key = Norm(row.TenantKey); var db = Db(); var cur = await db.Set().FirstOrDefaultAsync(x => x.TenantKey == key, ct); if (cur is null) return false; if (ifUnmodifiedSince.HasValue && cur.UpdatedAtUtc > ifUnmodifiedSince.Value) return false; cur.Schema = row.Schema is null ? cur.Schema : (string.IsNullOrWhiteSpace(row.Schema) ? null : row.Schema.Trim()); cur.ConnectionString = row.ConnectionString is null ? cur.ConnectionString : (string.IsNullOrWhiteSpace(row.ConnectionString) ? null : row.ConnectionString.Trim()); cur.Mode = row.Mode; cur.IsActive = row.IsActive; cur.UpdatedAtUtc = DateTimeOffset.UtcNow; return await db.SaveChangesAsync(ct) > 0; } public async Task DeleteAsync(string tenantKey, CancellationToken ct = default) { var key = Norm(tenantKey); var db = Db(); var t = await db.Set().FirstOrDefaultAsync(x => x.TenantKey == key, ct); if (t is null) return false; if (t.IsActive) { t.IsActive = false; t.UpdatedAtUtc = DateTimeOffset.UtcNow; } var domains = await db.Set() .Where(d => d.TenantKey == key && d.IsActive) .ToListAsync(ct); foreach (var d in domains) { d.IsActive = false; d.UpdatedAtUtc = DateTimeOffset.UtcNow; } return await db.SaveChangesAsync(ct) > 0; } public async Task MapDomainAsync(string domain, string tenantKey, CancellationToken ct = default) { var d = Norm(domain); var key = Norm(tenantKey); if (!await Db().Set().AnyAsync(x => x.TenantKey == key && x.IsActive, ct)) return false; var db = Db(); var ex = await db.Set().FirstOrDefaultAsync(x => x.Domain == d, ct); if (ex is null) await db.Set() .AddAsync( new TenantDomain { Domain = d, TenantKey = key, IsPlatformBaseDomain = false, IsActive = true, UpdatedAtUtc = DateTimeOffset.UtcNow }, ct); else { ex.TenantKey = key; ex.IsPlatformBaseDomain = false; ex.IsActive = true; ex.UpdatedAtUtc = DateTimeOffset.UtcNow; } return await db.SaveChangesAsync(ct) > 0; } public async Task UnmapDomainAsync(string domain, CancellationToken ct = default) { var d = Norm(domain); var db = Db(); var ex = await db.Set().FirstOrDefaultAsync(x => x.Domain == d, ct); if (ex is null) return false; ex.IsActive = false; ex.TenantKey = null; ex.IsPlatformBaseDomain = false; ex.UpdatedAtUtc = DateTimeOffset.UtcNow; return await db.SaveChangesAsync(ct) > 0; } public async Task> ListDomainsAsync(string? tenantKey, CancellationToken ct = default) { var db = Db(); var q = db.Set().AsNoTracking(); if (!string.IsNullOrWhiteSpace(tenantKey)) { var key = Norm(tenantKey!); q = q.Where(x => x.TenantKey == key || x.IsPlatformBaseDomain); } return await q.OrderBy(x => x.Domain).ToListAsync(ct); } public async Task AddBaseDomainAsync(string baseDomain, CancellationToken ct = default) { var d = Norm(baseDomain); var db = Db(); var ex = await db.Set().FirstOrDefaultAsync(x => x.Domain == d, ct); if (ex is null) await db.Set() .AddAsync( new TenantDomain { Domain = d, TenantKey = null, IsPlatformBaseDomain = true, IsActive = true, UpdatedAtUtc = DateTimeOffset.UtcNow }, ct); else { ex.TenantKey = null; ex.IsPlatformBaseDomain = true; ex.IsActive = true; ex.UpdatedAtUtc = DateTimeOffset.UtcNow; } return await db.SaveChangesAsync(ct) > 0; } public async Task RemoveBaseDomainAsync(string baseDomain, CancellationToken ct = default) { var d = Norm(baseDomain); var db = Db(); var ex = await db.Set() .FirstOrDefaultAsync(x => x.Domain == d && x.IsPlatformBaseDomain, ct); if (ex is null) return false; ex.IsActive = false; ex.UpdatedAtUtc = DateTimeOffset.UtcNow; return await db.SaveChangesAsync(ct) > 0; } /// /// คืนค่า TenantKey จาก TenantId (Guid) ถ้าไม่เจอหรือไม่ active คืน null /// public async Task GetTenantKeyByTenantIdAsync(Guid tenantId, CancellationToken ct = default) { var key = await Db().Set() .AsNoTracking() .Where(x => x.TenantId == tenantId && x.IsActive) .Select(x => x.TenantKey) .FirstOrDefaultAsync(ct); return string.IsNullOrWhiteSpace(key) ? null : key; } /// /// คืนค่าแม็พ TenantId -> TenantKey เฉพาะที่เจอและ active /// public async Task> MapTenantKeysAsync(IEnumerable tenantIds, CancellationToken ct = default) { var ids = (tenantIds ?? Array.Empty()).Distinct().ToArray(); if (ids.Length == 0) return new Dictionary(); var rows = await Db().Set() .AsNoTracking() .Where(x => ids.Contains(x.TenantId) && x.IsActive) .Select(x => new { x.TenantId, x.TenantKey }) .ToListAsync(ct); return rows.ToDictionary(x => x.TenantId, x => x.TenantKey); } }