diff --git a/src/Core/Repositories/IOrganizationDomainRepository.cs b/src/Core/Repositories/IOrganizationDomainRepository.cs index f8b45574a2..d802fe65df 100644 --- a/src/Core/Repositories/IOrganizationDomainRepository.cs +++ b/src/Core/Repositories/IOrganizationDomainRepository.cs @@ -12,6 +12,7 @@ public interface IOrganizationDomainRepository : IRepository> GetManyByNextRunDateAsync(DateTime date); Task GetOrganizationDomainSsoDetailsAsync(string email); Task> GetVerifiedOrganizationDomainSsoDetailsAsync(string email); + Task> GetVerifiedDomainsByOrganizationIdsAsync(IEnumerable organizationIds); Task GetDomainByIdOrganizationIdAsync(Guid id, Guid organizationId); Task GetDomainByOrgIdAndDomainNameAsync(Guid orgId, string domainName); Task> GetExpiredOrganizationDomainsAsync(); diff --git a/src/Infrastructure.Dapper/Repositories/OrganizationDomainRepository.cs b/src/Infrastructure.Dapper/Repositories/OrganizationDomainRepository.cs index 1a7085eb18..91cbc40ff6 100644 --- a/src/Infrastructure.Dapper/Repositories/OrganizationDomainRepository.cs +++ b/src/Infrastructure.Dapper/Repositories/OrganizationDomainRepository.cs @@ -46,6 +46,20 @@ public class OrganizationDomainRepository : Repository } } + public async Task> GetVerifiedDomainsByOrganizationIdsAsync(IEnumerable organizationIds) + { + + using (var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + $"[{Schema}].[OrganizationDomain_ReadByOrganizationIds]", + new { OrganizationIds = organizationIds.ToGuidIdArrayTVP() }, + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + } + public async Task> GetManyByNextRunDateAsync(DateTime date) { using var connection = new SqlConnection(ConnectionString); diff --git a/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs b/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs index 50d791b81b..e7bee0cdfd 100644 --- a/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs @@ -157,4 +157,25 @@ public class OrganizationDomainRepository : Repository 0; } + + public async Task> GetVerifiedDomainsByOrganizationIdsAsync( + IEnumerable organizationIds) + { + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + + var verifiedDomains = await (from d in dbContext.OrganizationDomains + where organizationIds.Contains(d.OrganizationId) && d.VerifiedDate != null + select new OrganizationDomain + { + OrganizationId = d.OrganizationId, + DomainName = d.DomainName + }) + .AsNoTracking() + .ToListAsync(); + + return Mapper.Map>(verifiedDomains); + } + } + diff --git a/src/Sql/dbo/Stored Procedures/OrganizationDomain_ReadByOrganizationIds.sql b/src/Sql/dbo/Stored Procedures/OrganizationDomain_ReadByOrganizationIds.sql new file mode 100644 index 0000000000..f62544e486 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationDomain_ReadByOrganizationIds.sql @@ -0,0 +1,14 @@ +CREATE PROCEDURE [dbo].[OrganizationDomain_ReadByOrganizationIds] + @OrganizationIds AS [dbo].[GuidIdArray] READONLY +AS +BEGIN + + SET NOCOUNT ON + + SELECT + d.OrganizationId, + d.DomainName + FROM dbo.OrganizationDomainView AS d + WHERE d.OrganizationId IN (SELECT [Id] FROM @OrganizationIds) + AND d.VerifiedDate IS NOT NULL; +END \ No newline at end of file diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationDomainRepositoryTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationDomainRepositoryTests.cs index a1c5f9bd07..6c1ac00073 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationDomainRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationDomainRepositoryTests.cs @@ -306,4 +306,81 @@ public class OrganizationDomainRepositoryTests var expectedDomain = domains.FirstOrDefault(domain => domain.DomainName == organizationDomain.DomainName); Assert.Null(expectedDomain); } + + [DatabaseTheory, DatabaseData] + public async Task GetVerifiedDomainsByOrganizationIdsAsync_ShouldVerifiedDomainsMatchesOrganizationIds( + IOrganizationRepository organizationRepository, + IOrganizationDomainRepository organizationDomainRepository) + { + // Arrange + var guid1 = Guid.NewGuid(); + var guid2 = Guid.NewGuid(); + + var organization1 = await organizationRepository.CreateAsync(new Organization + { + Name = $"Test Org {guid1}", + BillingEmail = $"test+{guid1}@example.com", + Plan = "Test", + PrivateKey = "privatekey", + + }); + + var organization1Domain1 = new OrganizationDomain + { + OrganizationId = organization1.Id, + DomainName = $"domain1+{guid1}@example.com", + Txt = "btw+12345" + }; + + const int arbitraryNextIteration = 1; + organization1Domain1.SetNextRunDate(arbitraryNextIteration); + organization1Domain1.SetVerifiedDate(); + + await organizationDomainRepository.CreateAsync(organization1Domain1); + + var organization1Domain2 = new OrganizationDomain + { + OrganizationId = organization1.Id, + DomainName = $"domain2+{guid1}@example.com", + Txt = "btw+12345" + }; + + organization1Domain2.SetNextRunDate(arbitraryNextIteration); + + await organizationDomainRepository.CreateAsync(organization1Domain2); + + var organization2 = await organizationRepository.CreateAsync(new Organization + { + Name = $"Test Org {guid2}", + BillingEmail = $"test+{guid2}@example.com", + Plan = "Test", + PrivateKey = "privatekey", + + }); + + var organization2Domain1 = new OrganizationDomain + { + OrganizationId = organization2.Id, + DomainName = $"domain+{guid2}@example.com", + Txt = "btw+12345" + }; + organization2Domain1.SetVerifiedDate(); + organization2Domain1.SetNextRunDate(arbitraryNextIteration); + + await organizationDomainRepository.CreateAsync(organization2Domain1); + + + // Act + var domains = await organizationDomainRepository.GetVerifiedDomainsByOrganizationIdsAsync(new[] { organization1.Id }); + + // Assert + var expectedDomain = domains.FirstOrDefault(domain => domain.DomainName == organization1Domain1.DomainName); + Assert.NotNull(expectedDomain); + + var unverifiedDomain = domains.FirstOrDefault(domain => domain.DomainName == organization1Domain2.DomainName); + var otherOrganizationDomain = domains.FirstOrDefault(domain => domain.DomainName == organization2Domain1.DomainName); + + Assert.Null(otherOrganizationDomain); + Assert.Null(unverifiedDomain); + } } diff --git a/util/Migrator/DbScripts/2025-02-17_00_OrganizationDomain_ReadByOrganizationIds.sql b/util/Migrator/DbScripts/2025-02-17_00_OrganizationDomain_ReadByOrganizationIds.sql new file mode 100644 index 0000000000..5616aa0ac7 --- /dev/null +++ b/util/Migrator/DbScripts/2025-02-17_00_OrganizationDomain_ReadByOrganizationIds.sql @@ -0,0 +1,15 @@ + +CREATE OR ALTER PROCEDURE [dbo].[OrganizationDomain_ReadByOrganizationIds] + @OrganizationIds AS [dbo].[GuidIdArray] READONLY +AS +BEGIN + + SET NOCOUNT ON + + SELECT + d.OrganizationId, + d.DomainName + FROM dbo.OrganizationDomainView AS d + WHERE d.OrganizationId IN (SELECT [Id] FROM @OrganizationIds) + AND d.VerifiedDate IS NOT NULL; +END \ No newline at end of file