From f91582befaa74f85173459ffe88f1aff929f66ba Mon Sep 17 00:00:00 2001 From: Peggy Date: Fri, 30 Aug 2024 15:38:44 -0700 Subject: [PATCH] controlling member invite and management (#1354) # Description This PR includes the following proposed change(s): - [spdbt-2679] 1. controlling member invite 2. return controlling member application status and invite status 3. remove controlling member connection with parentApp 4. move member management to bizMember out of bizLicApp --- src/Spd.Manager.Licence/BizLicAppContract.cs | 24 +-- src/Spd.Manager.Licence/BizLicAppManager.cs | 86 +++-------- src/Spd.Manager.Licence/BizMemberContract.cs | 37 +++++ src/Spd.Manager.Licence/BizMemberManager.cs | 137 +++++++++++++++++ .../BizMemberValidation.cs | 2 + .../LicenceAppManagerBase.cs | 2 +- src/Spd.Manager.Licence/Mappings.cs | 11 +- .../ApplicationContract.cs | 9 -- .../ApplicationManager.Invite.cs | 1 + src/Spd.Manager.Shared/Contract.cs | 11 ++ .../Controller/BizLicensingControllerTest.cs | 2 +- .../Controllers/BizLicensingController.cs | 44 +----- .../Controllers/BizMembersController.cs | 90 +++++++++++ .../SpdApplicantLicenceControllerBase.cs | 2 +- .../BizContactRepositoryTest.cs | 28 +--- .../ApplicationInvite/Contract.cs | 10 +- .../BizContact/BizContactRepository.cs | 46 ++---- .../BizContact/Contract.cs | 11 +- .../BizContact/Mappings.cs | 38 +++++ .../Contract.cs | 8 +- .../ControllingMemberCrcRepository.cs | 16 +- .../ControllingMemberInvite/Contract.cs | 49 ++++++ .../ControllingMemberInviteRepository.cs | 89 +++++++++++ .../ControllingMemberInvite/Mappings.cs | 34 ++++ .../ServiceExtensions.cs | 2 + src/Spd.Resource.Repository/SharedContract.cs | 10 ++ .../OData Service/ConnectedService.json | 2 +- .../OData Service/OData ServiceCsdl.xml | 10 +- .../OData Service/Reference.cs | 145 ++++++++++++++++-- .../DynamicsContextLookupHelpers.cs | 15 ++ src/Spd.Utilities.Dynamics/OptionSets.cs | 1 + src/Spd.Utilities.Shared/SpdConstants.cs | 1 + 32 files changed, 732 insertions(+), 241 deletions(-) create mode 100644 src/Spd.Manager.Licence/BizMemberContract.cs create mode 100644 src/Spd.Manager.Licence/BizMemberManager.cs create mode 100644 src/Spd.Manager.Licence/BizMemberValidation.cs create mode 100644 src/Spd.Presentation.Licensing/Controllers/BizMembersController.cs create mode 100644 src/Spd.Resource.Repository/ControllingMemberInvite/Contract.cs create mode 100644 src/Spd.Resource.Repository/ControllingMemberInvite/ControllingMemberInviteRepository.cs create mode 100644 src/Spd.Resource.Repository/ControllingMemberInvite/Mappings.cs diff --git a/src/Spd.Manager.Licence/BizLicAppContract.cs b/src/Spd.Manager.Licence/BizLicAppContract.cs index f38cb58ef..cea459b18 100644 --- a/src/Spd.Manager.Licence/BizLicAppContract.cs +++ b/src/Spd.Manager.Licence/BizLicAppContract.cs @@ -12,8 +12,6 @@ public interface IBizLicAppManager public Task Handle(BizLicAppReplaceCommand command, CancellationToken ct); public Task Handle(BizLicAppRenewCommand command, CancellationToken ct); public Task Handle(BizLicAppUpdateCommand command, CancellationToken ct); - public Task Handle(GetBizMembersQuery query, CancellationToken ct); - public Task Handle(UpsertBizMembersCommand cmd, CancellationToken ct); public Task> Handle(GetBizLicAppListQuery cmd, CancellationToken ct); public Task Handle(BrandImageQuery qry, CancellationToken ct); } @@ -99,6 +97,8 @@ public abstract record BizLicenceApp : LicenceAppBase public record NonSwlContactInfo : ContactInfo { public Guid? BizContactId { get; set; } + public ApplicationPortalStatusCode? ControllingMemberAppStatusCode { get; set; } + public ApplicationInviteStatusCode? InviteStatusCode { get; set; } } public record PrivateInvestigatorSwlContactInfo : ContactInfo @@ -107,23 +107,3 @@ public record PrivateInvestigatorSwlContactInfo : ContactInfo public Guid? BizContactId { get; set; } public Guid? LicenceId { get; set; } } - -public record GetBizMembersQuery(Guid BizId, Guid? AppId = null) : IRequest; - -public record Members -{ - public IEnumerable SwlControllingMembers { get; set; } - public IEnumerable NonSwlControllingMembers { get; set; } - public IEnumerable Employees { get; set; } -}; - -public record MembersRequest : Members -{ - public IEnumerable ControllingMemberDocumentKeyCodes { get; set; } = Array.Empty();//the document is saved in cache. -} - -public record UpsertBizMembersCommand( - Guid BizId, - Guid? ApplicationId, - Members Members, - IEnumerable LicAppFileInfos) : IRequest; \ No newline at end of file diff --git a/src/Spd.Manager.Licence/BizLicAppManager.cs b/src/Spd.Manager.Licence/BizLicAppManager.cs index 91ddb4941..72a00c6c0 100644 --- a/src/Spd.Manager.Licence/BizLicAppManager.cs +++ b/src/Spd.Manager.Licence/BizLicAppManager.cs @@ -27,8 +27,6 @@ internal class BizLicAppManager : IRequestHandler, IRequestHandler, IRequestHandler, - IRequestHandler, - IRequestHandler, IRequestHandler>, IRequestHandler, IBizLicAppManager @@ -73,10 +71,6 @@ public async Task Handle(GetBizLicAppQuery query, Cancellatio BizLicAppResponse result = _mapper.Map(response); var existingDocs = await _documentRepository.QueryAsync(new DocumentQry(query.LicenceApplicationId), cancellationToken); result.DocumentInfos = _mapper.Map(existingDocs.Items).Where(d => d.LicenceDocumentTypeCode != null).ToList(); - - if (result.BizId != null) - result.Members = await Handle(new GetBizMembersQuery((Guid)result.BizId, null), cancellationToken); - return result; } @@ -112,7 +106,6 @@ public async Task Handle(BizLicAppUpsertCommand cmd, C if (cmd.BizLicAppUpsertRequest.Members != null && cmd.BizLicAppUpsertRequest.BizId != null) await UpdateMembersAsync(cmd.BizLicAppUpsertRequest.Members, cmd.BizLicAppUpsertRequest.BizId, - (Guid)cmd.BizLicAppUpsertRequest.LicenceAppId, cancellationToken); if (cmd.BizLicAppUpsertRequest.DocumentInfos != null && cmd.BizLicAppUpsertRequest.DocumentInfos.Any()) @@ -162,7 +155,6 @@ public async Task Handle(BizLicAppReplaceCommand cmd, if (cmd.LicenceRequest.Members != null) await UpdateMembersAsync(cmd.LicenceRequest.Members, (Guid)originalLic.BizId, - (Guid)originalLic.LicenceAppId, cancellationToken); return new BizLicAppCommandResponse { LicenceAppId = response.LicenceAppId, Cost = cost }; @@ -209,7 +201,6 @@ await ValidateFilesForRenewUpdateAppAsync(cmd.LicenceRequest, if (cmd.LicenceRequest.Members != null) await UpdateMembersAsync(cmd.LicenceRequest.Members, (Guid)originalBizLic.BizId, - (Guid)originalBizLic.LicenceAppId, cancellationToken); // Upload new files @@ -303,55 +294,11 @@ await _documentRepository.ManageAsync( if (cmd.LicenceRequest.Members != null) await UpdateMembersAsync(cmd.LicenceRequest.Members, (Guid)originalLicApp.BizId, - (Guid)originalLicApp.LicenceAppId, cancellationToken); return new BizLicAppCommandResponse { LicenceAppId = response?.LicenceAppId ?? originalLicApp.LicenceAppId, Cost = cost }; } - public async Task Handle(GetBizMembersQuery qry, CancellationToken ct) - { - var bizMembers = await _bizContactRepository.GetBizAppContactsAsync(new BizContactQry(qry.BizId, null), ct); - Members members = new(); - members.SwlControllingMembers = bizMembers.Where(c => c.ContactId != null && c.LicenceId != null) - .Where(c => c.BizContactRoleCode == BizContactRoleEnum.ControllingMember) - .Select(c => new SwlContactInfo() - { - BizContactId = c.BizContactId, - LicenceId = c.LicenceId, - ContactId = c.ContactId, - }); - members.NonSwlControllingMembers = bizMembers.Where(c => c.ContactId == null && c.LicenceId == null) - .Where(c => c.BizContactRoleCode == BizContactRoleEnum.ControllingMember) - .Select(c => _mapper.Map(c)); - members.Employees = bizMembers.Where(c => c.ContactId != null && c.LicenceId != null) - .Where(c => c.BizContactRoleCode == BizContactRoleEnum.Employee) - .Select(c => _mapper.Map(c)); - return members; - } - - public async Task Handle(UpsertBizMembersCommand cmd, CancellationToken ct) - { - await UpdateMembersAsync(cmd.Members, cmd.BizId, cmd.ApplicationId, ct); - if (cmd.LicAppFileInfos.Any(f => f.LicenceDocumentTypeCode != LicenceDocumentTypeCode.CorporateRegistryDocument)) - throw new ApiException(HttpStatusCode.BadRequest, "Can only Upload Corporate Registry Document for management of controlling member."); - - if (cmd.LicAppFileInfos != null && cmd.LicAppFileInfos.Any()) - { - foreach (LicAppFileInfo licAppFile in cmd.LicAppFileInfos) - { - SpdTempFile? tempFile = _mapper.Map(licAppFile); - CreateDocumentCmd? fileCmd = _mapper.Map(licAppFile); - fileCmd.AccountId = cmd.BizId; - fileCmd.ApplicationId = cmd.ApplicationId; - fileCmd.TempFile = tempFile; - //create bcgov_documenturl and file - await _documentRepository.ManageAsync(fileCmd, ct); - } - } - return default; - } - public async Task> Handle(GetBizLicAppListQuery query, CancellationToken cancellationToken) { LicenceAppQuery q = new( @@ -418,21 +365,6 @@ public async Task Handle(BrandImageQuery qry, CancellationToken ct } } - private async Task UpdateMembersAsync(Members members, Guid bizId, Guid? appId, CancellationToken ct) - { - List contacts = _mapper.Map>(members.NonSwlControllingMembers); - contacts.AddRange(_mapper.Map>(members.SwlControllingMembers)); - IList employees = _mapper.Map>(members.Employees); - foreach (var e in employees) - { - e.BizContactRoleCode = BizContactRoleEnum.Employee; - } - contacts.AddRange(employees); - BizContactUpsertCmd upsertCmd = new(bizId, appId, contacts); - await _bizContactRepository.ManageBizContactsAsync(upsertCmd, ct); - return default; - } - private async Task ValidateFilesForRenewUpdateAppAsync(BizLicAppSubmitRequest request, IList newFileInfos, CancellationToken ct) @@ -612,6 +544,24 @@ private async Task SetBizManagerInfo( return createApp; } + + //todo: if fe can make seperate call to members and application, then we can give all members related info to BizMemberManager, then this function + //can be removed. + private async Task UpdateMembersAsync(Members members, Guid bizId, CancellationToken ct) + { + List contacts = _mapper.Map>(members.NonSwlControllingMembers); + contacts.AddRange(_mapper.Map>(members.SwlControllingMembers)); + IList employees = _mapper.Map>(members.Employees); + foreach (var e in employees) + { + e.BizContactRoleCode = BizContactRoleEnum.Employee; + } + contacts.AddRange(employees); + BizContactUpsertCmd upsertCmd = new(bizId, contacts); + await _bizContactRepository.ManageBizContactsAsync(upsertCmd, ct); + return default; + } + private sealed record ChangeSpec { public bool CategoriesChanged { get; set; } diff --git a/src/Spd.Manager.Licence/BizMemberContract.cs b/src/Spd.Manager.Licence/BizMemberContract.cs new file mode 100644 index 000000000..325f217ae --- /dev/null +++ b/src/Spd.Manager.Licence/BizMemberContract.cs @@ -0,0 +1,37 @@ +using MediatR; + +namespace Spd.Manager.Licence; + +public interface IBizMemberManager +{ + public Task Handle(GetBizMembersQuery query, CancellationToken ct); + public Task Handle(UpsertBizMembersCommand cmd, CancellationToken ct); + public Task Handle(BizControllingMemberNewInviteCommand command, CancellationToken ct); +} + +public record BizControllingMemberNewInviteCommand(Guid BizContactId, Guid UserId, string HostUrl) : IRequest; + +public record GetBizMembersQuery(Guid BizId, Guid? AppId = null) : IRequest; + +public record Members +{ + public IEnumerable SwlControllingMembers { get; set; } + public IEnumerable NonSwlControllingMembers { get; set; } + public IEnumerable Employees { get; set; } +}; + +public record MembersRequest : Members +{ + public IEnumerable ControllingMemberDocumentKeyCodes { get; set; } = Array.Empty();//the document is saved in cache. +} + +public record UpsertBizMembersCommand( + Guid BizId, + Guid? ApplicationId, + Members Members, + IEnumerable LicAppFileInfos) : IRequest; + +public record ControllingMemberInvitesCreateResponse(Guid BizContactId) +{ + public bool CreateSuccess { get; set; } +} \ No newline at end of file diff --git a/src/Spd.Manager.Licence/BizMemberManager.cs b/src/Spd.Manager.Licence/BizMemberManager.cs new file mode 100644 index 000000000..25dd9c701 --- /dev/null +++ b/src/Spd.Manager.Licence/BizMemberManager.cs @@ -0,0 +1,137 @@ +using AutoMapper; +using MediatR; +using Spd.Resource.Repository.Application; +using Spd.Resource.Repository.Biz; +using Spd.Resource.Repository.BizContact; +using Spd.Resource.Repository.BizLicApplication; +using Spd.Resource.Repository.ControllingMemberInvite; +using Spd.Resource.Repository.Document; +using Spd.Resource.Repository.LicApp; +using Spd.Resource.Repository.Licence; +using Spd.Resource.Repository.LicenceFee; +using Spd.Resource.Repository.PersonLicApplication; +using Spd.Resource.Repository.Tasks; +using Spd.Utilities.FileStorage; +using Spd.Utilities.Shared.Exceptions; +using System.Net; + +namespace Spd.Manager.Licence; +internal class BizMemberManager : + LicenceAppManagerBase, + IRequestHandler, + IRequestHandler, + IRequestHandler, + IBizMemberManager +{ + private readonly IBizLicApplicationRepository _bizLicApplicationRepository; + private readonly IBizContactRepository _bizContactRepository; + private readonly ITaskRepository _taskRepository; + private readonly IBizRepository _bizRepository; + private readonly IPersonLicApplicationRepository _personLicApplicationRepository; + private readonly IControllingMemberInviteRepository _cmInviteRepository; + + public BizMemberManager( + ILicenceRepository licenceRepository, + ILicAppRepository licAppRepository, + IMapper mapper, + IDocumentRepository documentUrlRepository, + ILicenceFeeRepository feeRepository, + IMainFileStorageService mainFileStorageService, + ITransientFileStorageService transientFileStorageService, + IBizContactRepository bizContactRepository, + IControllingMemberInviteRepository cmInviteRepository) + : base(mapper, + documentUrlRepository, + feeRepository, + licenceRepository, + mainFileStorageService, + transientFileStorageService, + licAppRepository) + { + _bizContactRepository = bizContactRepository; + _cmInviteRepository = cmInviteRepository; + } + public async Task Handle(BizControllingMemberNewInviteCommand cmd, CancellationToken cancellationToken) + { + //check if bizContact already has invitation + //todo: probably we do not need to check this. it should allow user to send out invite multiple times. + //var existingInvites = await _cmInviteRepository.QueryAsync(new ControllingMemberInviteQuery(cmd.BizContactId), cancellationToken); + //if (existingInvites.Any(i => i.Status == Resource.Repository.ApplicationInviteStatusEnum.Sent)) + // throw new ApiException(HttpStatusCode.BadRequest, "There is already an invite sent out."); + + //get info from bizContactId + BizContactResp contactResp = await _bizContactRepository.GetBizContactAsync(cmd.BizContactId, cancellationToken); + if (contactResp.BizContactRoleCode != BizContactRoleEnum.ControllingMember) + throw new ApiException(HttpStatusCode.BadRequest, "Cannot send out invitation for non-controlling member."); + if (string.IsNullOrWhiteSpace(contactResp.EmailAddress)) + throw new ApiException(HttpStatusCode.BadRequest, "Cannot send out invitation when there is no email address provided."); + if (contactResp.LatestControllingMemberCrcAppPortalStatusEnum != null) + throw new ApiException(HttpStatusCode.BadRequest, "This business contact already has a CRC application"); + //todo : how can we check if the CRC approved but it has been expired. + + var createCmd = _mapper.Map(contactResp); + createCmd.CreatedByUserId = cmd.UserId; + createCmd.HostUrl = cmd.HostUrl; + await _cmInviteRepository.ManageAsync(createCmd, cancellationToken); + + return new ControllingMemberInvitesCreateResponse(cmd.BizContactId) { CreateSuccess = true }; + } + + public async Task Handle(GetBizMembersQuery qry, CancellationToken ct) + { + var bizMembers = await _bizContactRepository.QueryBizContactsAsync(new BizContactQry(qry.BizId, null), ct); + Members members = new(); + members.SwlControllingMembers = bizMembers.Where(c => c.ContactId != null && c.LicenceId != null) + .Where(c => c.BizContactRoleCode == BizContactRoleEnum.ControllingMember) + .Select(c => new SwlContactInfo() + { + BizContactId = c.BizContactId, + LicenceId = c.LicenceId, + ContactId = c.ContactId, + }); + members.NonSwlControllingMembers = bizMembers.Where(c => c.ContactId == null && c.LicenceId == null) + .Where(c => c.BizContactRoleCode == BizContactRoleEnum.ControllingMember) + .Select(c => _mapper.Map(c)); + members.Employees = bizMembers.Where(c => c.ContactId != null && c.LicenceId != null) + .Where(c => c.BizContactRoleCode == BizContactRoleEnum.Employee) + .Select(c => _mapper.Map(c)); + return members; + } + + public async Task Handle(UpsertBizMembersCommand cmd, CancellationToken ct) + { + await UpdateMembersAsync(cmd.Members, cmd.BizId, ct); + if (cmd.LicAppFileInfos.Any(f => f.LicenceDocumentTypeCode != LicenceDocumentTypeCode.CorporateRegistryDocument)) + throw new ApiException(HttpStatusCode.BadRequest, "Can only Upload Corporate Registry Document for management of controlling member."); + + if (cmd.LicAppFileInfos != null && cmd.LicAppFileInfos.Any()) + { + foreach (LicAppFileInfo licAppFile in cmd.LicAppFileInfos) + { + SpdTempFile? tempFile = _mapper.Map(licAppFile); + CreateDocumentCmd? fileCmd = _mapper.Map(licAppFile); + fileCmd.AccountId = cmd.BizId; + fileCmd.ApplicationId = cmd.ApplicationId; + fileCmd.TempFile = tempFile; + //create bcgov_documenturl and file + await _documentRepository.ManageAsync(fileCmd, ct); + } + } + return default; + } + + private async Task UpdateMembersAsync(Members members, Guid bizId, CancellationToken ct) + { + List contacts = _mapper.Map>(members.NonSwlControllingMembers); + contacts.AddRange(_mapper.Map>(members.SwlControllingMembers)); + IList employees = _mapper.Map>(members.Employees); + foreach (var e in employees) + { + e.BizContactRoleCode = BizContactRoleEnum.Employee; + } + contacts.AddRange(employees); + BizContactUpsertCmd upsertCmd = new(bizId, contacts); + await _bizContactRepository.ManageBizContactsAsync(upsertCmd, ct); + return default; + } +} \ No newline at end of file diff --git a/src/Spd.Manager.Licence/BizMemberValidation.cs b/src/Spd.Manager.Licence/BizMemberValidation.cs new file mode 100644 index 000000000..1006c0ec0 --- /dev/null +++ b/src/Spd.Manager.Licence/BizMemberValidation.cs @@ -0,0 +1,2 @@ +namespace Spd.Manager.Licence; + diff --git a/src/Spd.Manager.Licence/LicenceAppManagerBase.cs b/src/Spd.Manager.Licence/LicenceAppManagerBase.cs index 279bd3db3..1bcfe1c67 100644 --- a/src/Spd.Manager.Licence/LicenceAppManagerBase.cs +++ b/src/Spd.Manager.Licence/LicenceAppManagerBase.cs @@ -199,7 +199,7 @@ protected async Task GetLatestApplicationId(Guid? contactId, Guid? bizId, .OrderByDescending(a => a.SubmittedOn) .FirstOrDefault(); if (app == null) - throw new ApiException(HttpStatusCode.BadRequest, $"there is no {licenceTypeEnum}."); + throw new ApiException(HttpStatusCode.InternalServerError, $"there is no completed {licenceTypeEnum} application."); return app.LicenceAppId; } diff --git a/src/Spd.Manager.Licence/Mappings.cs b/src/Spd.Manager.Licence/Mappings.cs index 49c500ac0..67d616c19 100644 --- a/src/Spd.Manager.Licence/Mappings.cs +++ b/src/Spd.Manager.Licence/Mappings.cs @@ -9,6 +9,7 @@ using Spd.Resource.Repository.BizLicApplication; using Spd.Resource.Repository.Contact; using Spd.Resource.Repository.ControllingMemberCrcApplication; +using Spd.Resource.Repository.ControllingMemberInvite; using Spd.Resource.Repository.Document; using Spd.Resource.Repository.LicApp; using Spd.Resource.Repository.Licence; @@ -318,11 +319,12 @@ public Mappings() .ReverseMap(); CreateMap() + .ForMember(d => d.ControllingMemberAppStatusCode, opt => opt.MapFrom(s => s.LatestControllingMemberCrcAppPortalStatusEnum)) + .ForMember(d => d.InviteStatusCode, opt => opt.MapFrom(s => s.LatestControllingMemberInvitationStatusEnum)) .ReverseMap() .ForMember(d => d.BizContactRoleCode, opt => opt.MapFrom(s => BizContactRoleEnum.ControllingMember)); - CreateMap() - .ForMember(d => d.AppId, opt => opt.MapFrom(s => s.ApplicationId)); + CreateMap(); CreateMap() .ForMember(d => d.BizContactRoleCode, opt => opt.MapFrom(s => BizContactRoleEnum.ControllingMember)) @@ -345,7 +347,10 @@ public Mappings() .ForPath(d => d.ResidentialAddressData.Country, opt => opt.MapFrom(s => s.ResidentialAddress.Country)); CreateMap(); - + CreateMap(); + CreateMap() + .IncludeBase() + .ForMember(d => d.HostUrl, opt => opt.Ignore()); } private static WorkerCategoryTypeEnum[] GetCategories(IEnumerable codes) diff --git a/src/Spd.Manager.Screening/ApplicationContract.cs b/src/Spd.Manager.Screening/ApplicationContract.cs index fd671784f..f41dedd5c 100644 --- a/src/Spd.Manager.Screening/ApplicationContract.cs +++ b/src/Spd.Manager.Screening/ApplicationContract.cs @@ -125,15 +125,6 @@ public record ApplicationInviteResponse : ApplicationInvite public string? ErrorMsg { get; set; } public bool? Viewed { get; set; } } - public enum ApplicationInviteStatusCode - { - Draft, - Sent, - Failed, - Completed, //inactive Status code - Cancelled,//inactive Status code - Expired //inactive Status code - } #endregion #region application diff --git a/src/Spd.Manager.Screening/ApplicationManager.Invite.cs b/src/Spd.Manager.Screening/ApplicationManager.Invite.cs index f807fc164..bfe621c28 100644 --- a/src/Spd.Manager.Screening/ApplicationManager.Invite.cs +++ b/src/Spd.Manager.Screening/ApplicationManager.Invite.cs @@ -1,5 +1,6 @@ using MediatR; using Spd.Engine.Validation; +using Spd.Resource.Repository; using Spd.Resource.Repository.ApplicationInvite; using Spd.Resource.Repository.Org; using Spd.Utilities.Shared; diff --git a/src/Spd.Manager.Shared/Contract.cs b/src/Spd.Manager.Shared/Contract.cs index 329206e31..713f511a0 100644 --- a/src/Spd.Manager.Shared/Contract.cs +++ b/src/Spd.Manager.Shared/Contract.cs @@ -129,6 +129,17 @@ public enum ServiceTypeCode MDRA, SECURITY_BUSINESS_LICENCE_CONTROLLING_MEMBER_CRC } + + public enum ApplicationInviteStatusCode + { + Draft, + Sent, + Failed, + Completed, //inactive Status code + Cancelled,//inactive Status code + Expired //inactive Status code + } + public record FileResponse { public string ContentType { get; set; } = null!; diff --git a/src/Spd.Presentation.Licensing.UnitTest/Controller/BizLicensingControllerTest.cs b/src/Spd.Presentation.Licensing.UnitTest/Controller/BizLicensingControllerTest.cs index c94443f4a..a7e237840 100644 --- a/src/Spd.Presentation.Licensing.UnitTest/Controller/BizLicensingControllerTest.cs +++ b/src/Spd.Presentation.Licensing.UnitTest/Controller/BizLicensingControllerTest.cs @@ -96,7 +96,7 @@ public async void GetLatestBizLicenceApplication_ReturnBizLicAppResponse() [Fact] public async void Get_GetBizLicenceApplication_Return_BizLicAppResponse() { - var result = await sut.GetBizLicenceApplication(Guid.NewGuid()); + var result = await sut.GetBizLicenceApplication(Guid.NewGuid(), CancellationToken.None); Assert.IsType(result); mockMediator.Verify(); diff --git a/src/Spd.Presentation.Licensing/Controllers/BizLicensingController.cs b/src/Spd.Presentation.Licensing/Controllers/BizLicensingController.cs index 1f981e841..a8edab9d8 100644 --- a/src/Spd.Presentation.Licensing/Controllers/BizLicensingController.cs +++ b/src/Spd.Presentation.Licensing/Controllers/BizLicensingController.cs @@ -46,9 +46,12 @@ public BizLicensingController(IPrincipal currentUser, [Route("api/business-licence-application/{licenceAppId}")] [Authorize(Policy = "OnlyBceid", Roles = "PrimaryBusinessManager,BusinessManager")] [HttpGet] - public async Task GetBizLicenceApplication([FromRoute][Required] Guid licenceAppId) + public async Task GetBizLicenceApplication([FromRoute][Required] Guid licenceAppId, CancellationToken ct) { - return await _mediator.Send(new GetBizLicAppQuery(licenceAppId)); + BizLicAppResponse response = await _mediator.Send(new GetBizLicAppQuery(licenceAppId)); + if (response.BizId != null) + response.Members = await _mediator.Send(new GetBizMembersQuery((Guid)response.BizId, null), ct); + return response; } /// @@ -143,43 +146,6 @@ public async Task> UploadLicenceAppFiles return response; } - /// - /// Get Biz controlling members and employees, controlling member includes swl and non-swl - /// This is the latest active biz controlling members and employees, irrelevent to application. - /// - /// - /// - /// - /// - [Route("api/business-licence-application/{bizId}/members")] - [HttpGet] - [Authorize(Policy = "OnlyBceid", Roles = "PrimaryBusinessManager,BusinessManager")] - public async Task GetMembers([FromRoute] Guid bizId, CancellationToken ct) - { - return await _mediator.Send(new GetBizMembersQuery(bizId), ct); - } - - /// - /// Upsert Biz Application controlling members and employees, controlling members include swl and non-swl - /// - /// - /// - /// - /// - [Route("api/business-licence-application/{bizId}/members")] - [HttpPost] - [Authorize(Policy = "OnlyBceid", Roles = "PrimaryBusinessManager,BusinessManager")] - public async Task UpsertMembers([FromRoute] Guid bizId, [FromBody] MembersRequest members, CancellationToken ct) - { - IEnumerable newDocInfos = await GetAllNewDocsInfoAsync(members.ControllingMemberDocumentKeyCodes, ct); - if (newDocInfos.Count() != members.ControllingMemberDocumentKeyCodes.Count()) - { - throw new ApiException(HttpStatusCode.BadRequest, "Cannot find all files in the cache."); - } - await _mediator.Send(new UpsertBizMembersCommand(bizId, null, members, newDocInfos), ct); - return Ok(); - } - /// /// Uploading file only save files in cache, the files are not connected to the biz and application yet. /// this is used for uploading member files or update, renew, replace. diff --git a/src/Spd.Presentation.Licensing/Controllers/BizMembersController.cs b/src/Spd.Presentation.Licensing/Controllers/BizMembersController.cs new file mode 100644 index 000000000..bc7140b0f --- /dev/null +++ b/src/Spd.Presentation.Licensing/Controllers/BizMembersController.cs @@ -0,0 +1,90 @@ +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Caching.Distributed; +using Spd.Manager.Licence; +using Spd.Utilities.LogonUser; +using Spd.Utilities.Recaptcha; +using Spd.Utilities.Shared.Exceptions; +using System.ComponentModel.DataAnnotations; +using System.Configuration; +using System.Net; +using System.Security.Principal; + +namespace Spd.Presentation.Licensing.Controllers +{ + [ApiController] + public class BizMembersController : SpdLicenceControllerBase + { + private readonly IPrincipal _currentUser; + private readonly IMediator _mediator; + + public BizMembersController(IPrincipal currentUser, + IMediator mediator, + IConfiguration configuration, + IRecaptchaVerificationService recaptchaVerificationService, + IDistributedCache cache, + IDataProtectionProvider dpProvider) : base(cache, dpProvider, recaptchaVerificationService, configuration) + { + _currentUser = currentUser; + _mediator = mediator; + } + + /// + /// Get Biz controlling members and employees, controlling member includes swl and non-swl + /// This is the latest active biz controlling members and employees, irrelevent to application. + /// + /// + /// + /// + /// + [Route("api/business-licence-application/{bizId}/members")] + [HttpGet] + [Authorize(Policy = "OnlyBceid", Roles = "PrimaryBusinessManager,BusinessManager")] + public async Task GetMembers([FromRoute] Guid bizId, CancellationToken ct) + { + return await _mediator.Send(new GetBizMembersQuery(bizId), ct); + } + + /// + /// Upsert Biz Application controlling members and employees, controlling members include swl and non-swl + /// + /// + /// + /// + /// + [Route("api/business-licence-application/{bizId}/members")] + [HttpPost] + [Authorize(Policy = "OnlyBceid", Roles = "PrimaryBusinessManager,BusinessManager")] + public async Task UpsertMembers([FromRoute] Guid bizId, [FromBody] MembersRequest members, CancellationToken ct) + { + IEnumerable newDocInfos = await GetAllNewDocsInfoAsync(members.ControllingMemberDocumentKeyCodes, ct); + if (newDocInfos.Count() != members.ControllingMemberDocumentKeyCodes.Count()) + { + throw new ApiException(HttpStatusCode.BadRequest, "Cannot find all files in the cache."); + } + await _mediator.Send(new UpsertBizMembersCommand(bizId, null, members, newDocInfos), ct); + return Ok(); + } + + /// + /// Create controlling member crc invitation for this biz contact + /// + /// + /// + [Route("api/business-licence-application/controlling-member-invitation/{bizContactId}")] + [Authorize(Policy = "OnlyBceid", Roles = "PrimaryBusinessManager,BusinessManager")] + [HttpGet] + public async Task CreateControllingMemberCrcAppInvitation([FromRoute][Required] Guid bizContactId, CancellationToken ct) + { + var userIdStr = _currentUser.GetUserId(); + if (userIdStr == null) throw new ApiException(System.Net.HttpStatusCode.Unauthorized); + string? hostUrl = _configuration.GetValue("HostUrl"); + if (hostUrl == null) + throw new ConfigurationErrorsException("HostUrl is not set correctly in configuration."); + var inviteCreateCmd = new BizControllingMemberNewInviteCommand(bizContactId, Guid.Parse(userIdStr), hostUrl); + return await _mediator.Send(inviteCreateCmd, ct); + } + } +} \ No newline at end of file diff --git a/src/Spd.Presentation.Licensing/Controllers/SpdApplicantLicenceControllerBase.cs b/src/Spd.Presentation.Licensing/Controllers/SpdApplicantLicenceControllerBase.cs index 793a59e6a..4f6723eea 100644 --- a/src/Spd.Presentation.Licensing/Controllers/SpdApplicantLicenceControllerBase.cs +++ b/src/Spd.Presentation.Licensing/Controllers/SpdApplicantLicenceControllerBase.cs @@ -16,7 +16,7 @@ public abstract class SpdLicenceControllerBase : SpdControllerBase private readonly ITimeLimitedDataProtector _dataProtector; private readonly IDistributedCache _cache; private readonly IRecaptchaVerificationService _recaptchaVerificationService; - private readonly IConfiguration _configuration; + protected readonly IConfiguration _configuration; protected SpdLicenceControllerBase(IDistributedCache cache, IDataProtectionProvider dpProvider, diff --git a/src/Spd.Resource.Repository.IntegrationTest/BizContactRepositoryTest.cs b/src/Spd.Resource.Repository.IntegrationTest/BizContactRepositoryTest.cs index 5efc1b817..05660f0c2 100644 --- a/src/Spd.Resource.Repository.IntegrationTest/BizContactRepositoryTest.cs +++ b/src/Spd.Resource.Repository.IntegrationTest/BizContactRepositoryTest.cs @@ -21,8 +21,8 @@ public async Task GetBizAppContactsAsync_return_Correctly() { account biz = await CreateAccountAsync(); spd_application app = await CreateApplicationAsync(biz); - spd_businesscontact bizContact = await CreateBizContactAsync(biz, app, "firstName1", BizContactRoleOptionSet.ControllingMember); - spd_businesscontact bizContact2 = await CreateBizContactAsync(biz, app, "firstName2", BizContactRoleOptionSet.Employee); + spd_businesscontact bizContact = await CreateBizContactAsync(biz, "firstName1", BizContactRoleOptionSet.ControllingMember); + spd_businesscontact bizContact2 = await CreateBizContactAsync(biz, "firstName2", BizContactRoleOptionSet.Employee); await _context.SaveChangesAsync(CancellationToken.None); try @@ -31,7 +31,7 @@ public async Task GetBizAppContactsAsync_return_Correctly() BizContactQry qry = new(biz.accountid, app.spd_applicationid); // Action - var response = await _bizContactRepo.GetBizAppContactsAsync(qry, CancellationToken.None); + var response = await _bizContactRepo.QueryBizContactsAsync(qry, CancellationToken.None); // Assert Assert.NotNull(response); @@ -58,10 +58,9 @@ public async Task ManageBizContactsAsync_WithNoExistingContacts_Correctly() // Arrange //create account account biz = await CreateAccountAsync(); - spd_application app = await CreateApplicationAsync(biz); await _context.SaveChangesAsync(CancellationToken.None); - BizContactUpsertCmd cmd = new((Guid)biz.accountid, (Guid)app.spd_applicationid, new List()); + BizContactUpsertCmd cmd = new((Guid)biz.accountid, new List()); try { @@ -77,7 +76,6 @@ public async Task ManageBizContactsAsync_WithNoExistingContacts_Correctly() finally { //Annihilate - _context.DeleteObject(app); _context.DeleteObject(biz); await _context.SaveChangesAsync(CancellationToken.None); } @@ -142,11 +140,9 @@ public async Task ManageBizContactsAsync_WithExistingBizContacts_Correctly() { // Arrange account biz = await CreateAccountAsync(); - spd_application app = await CreateApplicationAsync(biz); await _context.SaveChangesAsync(CancellationToken.None); - spd_businesscontact bizContact = await CreateBizContactAsync(biz, app, "firstName1", BizContactRoleOptionSet.ControllingMember); - spd_businesscontact bizContact2 = await CreateBizContactAsync(biz, app, "firstName2", BizContactRoleOptionSet.ControllingMember); - spd_application newApp = await CreateApplicationAsync(biz); + spd_businesscontact bizContact = await CreateBizContactAsync(biz, "firstName1", BizContactRoleOptionSet.ControllingMember); + spd_businesscontact bizContact2 = await CreateBizContactAsync(biz, "firstName2", BizContactRoleOptionSet.ControllingMember); await _context.SaveChangesAsync(CancellationToken.None); try @@ -157,7 +153,7 @@ public async Task ManageBizContactsAsync_WithExistingBizContacts_Correctly() new BizContactResp{ GivenName = "newFirstName3", EmailAddress="firstName3@add.com", BizContactRoleCode=BizContactRoleEnum.ControllingMember}, }; - BizContactUpsertCmd cmd = new((Guid)biz.accountid, (Guid)newApp.spd_applicationid, requests); + BizContactUpsertCmd cmd = new((Guid)biz.accountid, requests); // Action var response = await _bizContactRepo.ManageBizContactsAsync(cmd, CancellationToken.None); @@ -169,19 +165,12 @@ public async Task ManageBizContactsAsync_WithExistingBizContacts_Correctly() .ToList(); Assert.Equal(2, bizContacts.Count()); Assert.Equal(true, bizContacts.Any(c => c.spd_firstname == "newFirstName1")); //updated - - var newAppliation = _context.spd_applications - .Expand(a => a.spd_businesscontact_spd_application) - .Where(a => a.spd_applicationid == newApp.spd_applicationid) - .FirstOrDefault(); - Assert.Equal(2, newAppliation.spd_businesscontact_spd_application.Count()); } finally { //Annihilate _context.DeleteObject(bizContact); _context.DeleteObject(bizContact2); - _context.DeleteObject(app); _context.DeleteObject(biz); await _context.SaveChangesAsync(CancellationToken.None); } @@ -206,7 +195,7 @@ private async Task CreateApplicationAsync(account biz) return app; } - private async Task CreateBizContactAsync(account biz, spd_application app, string firstName, BizContactRoleOptionSet role) + private async Task CreateBizContactAsync(account biz, string firstName, BizContactRoleOptionSet role) { spd_businesscontact bizContact = new(); bizContact.spd_businesscontactid = Guid.NewGuid(); @@ -214,7 +203,6 @@ private async Task CreateBizContactAsync(account biz, spd_a bizContact.spd_role = (int)role; _context.AddTospd_businesscontacts(bizContact); _context.SetLink(bizContact, nameof(bizContact.spd_OrganizationId), biz); - _context.AddLink(bizContact, nameof(bizContact.spd_businesscontact_spd_application), app); return bizContact; } diff --git a/src/Spd.Resource.Repository/ApplicationInvite/Contract.cs b/src/Spd.Resource.Repository/ApplicationInvite/Contract.cs index 0aa8858a5..925d2de56 100644 --- a/src/Spd.Resource.Repository/ApplicationInvite/Contract.cs +++ b/src/Spd.Resource.Repository/ApplicationInvite/Contract.cs @@ -104,13 +104,5 @@ public enum ScreenTypeEnum Licensee } - public enum ApplicationInviteStatusEnum - { - Draft, - Sent, - Failed, - Completed, - Cancelled, - Expired - } + } diff --git a/src/Spd.Resource.Repository/BizContact/BizContactRepository.cs b/src/Spd.Resource.Repository/BizContact/BizContactRepository.cs index 1e067c87e..849dec0f0 100644 --- a/src/Spd.Resource.Repository/BizContact/BizContactRepository.cs +++ b/src/Spd.Resource.Repository/BizContact/BizContactRepository.cs @@ -23,27 +23,24 @@ public BizContactRepository(IDynamicsContextFactory ctx, _logger = logger; } - public async Task> GetBizAppContactsAsync(BizContactQry qry, CancellationToken ct) + public async Task GetBizContactAsync(Guid bizContactId, CancellationToken ct) + { + spd_businesscontact? bizContact = await _context.spd_businesscontacts + .Expand(c => c.spd_businesscontact_spd_application) + .Where(c => c.spd_businesscontactid == bizContactId) + .FirstOrDefaultAsync(ct); + + if (bizContact == null) + throw new ApiException(HttpStatusCode.BadRequest, "Cannot find the business contact"); + + return _mapper.Map(bizContact); + } + + public async Task> QueryBizContactsAsync(BizContactQry qry, CancellationToken ct) { IQueryable bizContacts = _context.spd_businesscontacts - .Expand(c => c.spd_businesscontact_spd_application); - if (qry.AppId != null) //change to n:n relationship, so have to do the separate way. - { - spd_application? app = _context.spd_applications.Expand(a => a.spd_businesscontact_spd_application) - .Where(a => a.spd_applicationid == qry.AppId) - .FirstOrDefault(); - if (app != null) - { - IList bizContactList = app.spd_businesscontact_spd_application.ToList(); - if (!qry.IncludeInactive) - { - bizContactList = bizContactList.Where(a => a.statecode != DynamicsConstants.StateCode_Inactive).ToList(); - } - if (qry.RoleCode != null) - bizContactList = bizContactList.Where(a => a.spd_role == (int?)SharedMappingFuncs.GetOptionset(qry.RoleCode)).ToList(); - return _mapper.Map>(bizContactList); - } - } + .Expand(c => c.spd_businesscontact_spd_application) + .Expand(c => c.spd_businesscontact_spd_portalinvitation); if (!qry.IncludeInactive) bizContacts = bizContacts.Where(a => a.statecode != DynamicsConstants.StateCode_Inactive); @@ -72,21 +69,12 @@ public async Task ManageBizContactsAsync(BizContactUpsertCmd cmd, Cancella _context.UpdateObject(item); } - spd_application? app = null; - if (cmd.AppId != null) - { - app = await _context.GetApplicationById((Guid)cmd.AppId, ct); - if (app == null) throw new ApiException(HttpStatusCode.BadRequest, $"Application {cmd.AppId} does not exist."); - } - //update all that in cmd.Data var toModify = list.Where(c => cmd.Data.Any(d => d.BizContactId == c.spd_businesscontactid)); foreach (var item in toModify) { _mapper.Map(cmd.Data.FirstOrDefault(d => d.BizContactId == item.spd_businesscontactid), item); _context.UpdateObject(item); - if (app != null && !item.spd_businesscontact_spd_application.Any(a => a.spd_applicationid == cmd.AppId)) - _context.AddLink(item, nameof(item.spd_businesscontact_spd_application), app); } await _context.SaveChangesAsync(ct); @@ -125,8 +113,6 @@ public async Task ManageBizContactsAsync(BizContactUpsertCmd cmd, Cancella } _context.SetLink(bizContact, nameof(bizContact.spd_OrganizationId), biz); - if (app != null) - _context.AddLink(bizContact, nameof(bizContact.spd_businesscontact_spd_application), app); } } await _context.SaveChangesAsync(ct); diff --git a/src/Spd.Resource.Repository/BizContact/Contract.cs b/src/Spd.Resource.Repository/BizContact/Contract.cs index ec04b13f6..f38875ba5 100644 --- a/src/Spd.Resource.Repository/BizContact/Contract.cs +++ b/src/Spd.Resource.Repository/BizContact/Contract.cs @@ -1,14 +1,16 @@ using MediatR; +using Spd.Resource.Repository.Application; namespace Spd.Resource.Repository.BizContact { public interface IBizContactRepository { - Task> GetBizAppContactsAsync(BizContactQry qry, CancellationToken ct); + Task GetBizContactAsync(Guid bizContactId, CancellationToken ct); + Task> QueryBizContactsAsync(BizContactQry qry, CancellationToken ct); Task ManageBizContactsAsync(BizContactUpsertCmd cmd, CancellationToken ct); } //command - public record BizContactUpsertCmd(Guid BizId, Guid? AppId, List Data); + public record BizContactUpsertCmd(Guid BizId, List Data); //query public record BizContactQry(Guid? BizId, Guid? AppId, BizContactRoleEnum? RoleCode = null, bool IncludeInactive = false); @@ -25,6 +27,11 @@ public record BizContactResp public Guid? ContactId { get; set; } public Guid? LicenceId { get; set; } public BizContactRoleEnum BizContactRoleCode { get; set; } = BizContactRoleEnum.ControllingMember; + public Guid BizId { get; set; } + public Guid? LatestControllingMemberCrcAppId { get; set; } + public ApplicationPortalStatusEnum? LatestControllingMemberCrcAppPortalStatusEnum { get; set; } + public Guid? LatestControllingMemberInvitationId { get; set; } + public ApplicationInviteStatusEnum? LatestControllingMemberInvitationStatusEnum { get; set; } } public enum BizContactRoleEnum diff --git a/src/Spd.Resource.Repository/BizContact/Mappings.cs b/src/Spd.Resource.Repository/BizContact/Mappings.cs index 33a8f24ef..c7200a20f 100644 --- a/src/Spd.Resource.Repository/BizContact/Mappings.cs +++ b/src/Spd.Resource.Repository/BizContact/Mappings.cs @@ -1,5 +1,6 @@ using AutoMapper; using Microsoft.Dynamics.CRM; +using Spd.Resource.Repository.Application; using Spd.Utilities.Dynamics; namespace Spd.Resource.Repository.BizContact @@ -18,9 +19,46 @@ public Mappings() .ForMember(d => d.MiddleName2, opt => opt.MapFrom(s => s.spd_middlename2)) .ForMember(d => d.ContactId, opt => opt.MapFrom(s => s._spd_contactid_value)) .ForMember(d => d.LicenceId, opt => opt.MapFrom(s => s._spd_swlnumber_value)) + .ForMember(d => d.BizId, opt => opt.MapFrom(s => s._spd_organizationid_value)) + .ForMember(d => d.LatestControllingMemberInvitationId, opt => opt.MapFrom(s => GetLastestControllingMemberInvite(s.spd_businesscontact_spd_portalinvitation.ToList()).InviteId)) + .ForMember(d => d.LatestControllingMemberInvitationStatusEnum, opt => opt.MapFrom(s => GetLastestControllingMemberInvite(s.spd_businesscontact_spd_portalinvitation.ToList()).InviteStatus)) + .ForMember(d => d.LatestControllingMemberCrcAppId, opt => opt.MapFrom(s => GetLastestControllingMemberCrcApp(s.spd_businesscontact_spd_application.ToList()).AppId)) + .ForMember(d => d.LatestControllingMemberCrcAppPortalStatusEnum, opt => opt.MapFrom(s => GetLastestControllingMemberCrcApp(s.spd_businesscontact_spd_application.ToList()).PortalStatus)) .ReverseMap() .ForMember(d => d.spd_role, opt => opt.MapFrom(s => SharedMappingFuncs.GetOptionset(s.BizContactRoleCode))) .ForMember(d => d.spd_fullname, opt => opt.MapFrom(s => $"{s.Surname}, {s.GivenName}")); } + + private (Guid? AppId, ApplicationPortalStatusEnum? PortalStatus) GetLastestControllingMemberCrcApp(IEnumerable apps) + { + spd_application? app = apps.OrderByDescending(app => app.createdon).FirstOrDefault(); + if (app == null) return (null, null); + else + { + if (app.spd_portalstatus == null) return (app.spd_applicationid, null); + else + { + string status = ((ApplicationPortalStatus)app.spd_portalstatus.Value).ToString(); + ApplicationPortalStatusEnum statusEnum = Enum.Parse(status); + return (app.spd_applicationid, statusEnum); + } + } + } + + private (Guid? InviteId, ApplicationInviteStatusEnum? InviteStatus) GetLastestControllingMemberInvite(IEnumerable invites) + { + spd_portalinvitation? invite = invites.OrderByDescending(app => app.createdon).FirstOrDefault(); + if (invite == null) return (null, null); + else + { + if (invite.statuscode == null) return (invite.spd_portalinvitationid, null); + else + { + string status = ((InvitationStatus)invite.statuscode.Value).ToString(); + ApplicationInviteStatusEnum statusEnum = Enum.Parse(status); + return (invite.spd_portalinvitationid, statusEnum); + } + } + } } } diff --git a/src/Spd.Resource.Repository/ControllingMemberCrcApplication/Contract.cs b/src/Spd.Resource.Repository/ControllingMemberCrcApplication/Contract.cs index bb020f9dd..6d9954fe8 100644 --- a/src/Spd.Resource.Repository/ControllingMemberCrcApplication/Contract.cs +++ b/src/Spd.Resource.Repository/ControllingMemberCrcApplication/Contract.cs @@ -1,12 +1,6 @@ -using Spd.Resource.Repository.Alias; +using Spd.Resource.Repository.Alias; using Spd.Resource.Repository.Application; -using Spd.Resource.Repository.LicApp; using Spd.Resource.Repository.PersonLicApplication; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Spd.Resource.Repository.ControllingMemberCrcApplication; public partial interface IControllingMemberCrcRepository diff --git a/src/Spd.Resource.Repository/ControllingMemberCrcApplication/ControllingMemberCrcRepository.cs b/src/Spd.Resource.Repository/ControllingMemberCrcApplication/ControllingMemberCrcRepository.cs index 81853a4ca..d415f0ef0 100644 --- a/src/Spd.Resource.Repository/ControllingMemberCrcApplication/ControllingMemberCrcRepository.cs +++ b/src/Spd.Resource.Repository/ControllingMemberCrcApplication/ControllingMemberCrcRepository.cs @@ -1,17 +1,6 @@ -using AutoMapper; +using AutoMapper; using Microsoft.Dynamics.CRM; -using Spd.Resource.Repository.Alias; -using Spd.Resource.Repository.Application; -using Spd.Resource.Repository.BizLicApplication; -using Spd.Resource.Repository.LicApp; -using Spd.Resource.Repository.Org; -using Spd.Resource.Repository.PersonLicApplication; using Spd.Utilities.Dynamics; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Spd.Resource.Repository.ControllingMemberCrcApplication; public class ControllingMemberCrcRepository : IControllingMemberCrcRepository @@ -26,6 +15,7 @@ public ControllingMemberCrcRepository(IDynamicsContextFactory ctx, IMapper mappe _mapper = mapper; } + //for unauth, create contact and application public async Task CreateControllingMemberCrcApplicationAsync(CreateControllingMemberCrcAppCmd cmd, CancellationToken ct) { // get parent business license application @@ -141,7 +131,7 @@ public async Task SaveControllingMemberC } await _context.SaveChangesAsync(); - return new ControllingMemberCrcApplicationCmdResp((Guid)app.spd_applicationid, (Guid)cmd.ContactId); + return new ControllingMemberCrcApplicationCmdResp((Guid)app.spd_applicationid, cmd.ContactId); } } diff --git a/src/Spd.Resource.Repository/ControllingMemberInvite/Contract.cs b/src/Spd.Resource.Repository/ControllingMemberInvite/Contract.cs new file mode 100644 index 000000000..dec1317c6 --- /dev/null +++ b/src/Spd.Resource.Repository/ControllingMemberInvite/Contract.cs @@ -0,0 +1,49 @@ +namespace Spd.Resource.Repository.ControllingMemberInvite +{ + public interface IControllingMemberInviteRepository + { + public Task> QueryAsync(ControllingMemberInviteQuery query, CancellationToken cancellationToken); + public Task ManageAsync(ControllingMemberInviteCmd cmd, CancellationToken cancellationToken); + public Task VerifyControllingMemberInvitesAsync(ControllingMemberInviteVerifyCmd createInviteCmd, CancellationToken cancellationToken); + } + + public record ControllingMemberInviteQuery(Guid BizContactId, bool IncludeInactive = false); + public interface ControllingMemberInviteCmd { }; + public record ControllingMemberInviteCreateCmd : ControllingMemberInvite, ControllingMemberInviteCmd + { + public string HostUrl { get; set; } = null!; + }; + public record ControllingMemberInviteUpdateCmd : ControllingMemberInviteCmd + { + public Guid ControllingMemberInviteId { get; set; } + public ApplicationInviteStatusEnum ApplicationInviteStatusEnum { get; set; } + } + + public record ControllingMemberInviteResp : ControllingMemberInvite + { + public Guid Id { get; set; } + public DateTimeOffset CreatedOn { get; set; } + public ApplicationInviteStatusEnum Status { get; set; } + public string? ErrorMsg { get; set; } + public bool? Viewed { get; set; } + } + + public record ControllingMemberInviteVerifyCmd + { + } + + public record ControllingMemberInviteVerifyResp + { } + + public record ControllingMemberInvite + { + public string? GivenName { get; set; } + public string? Surname { get; set; } + public string? MiddleName1 { get; set; } + public string? MiddleName2 { get; set; } + public string? EmailAddress { get; set; } + public Guid BizId { get; set; } + public Guid CreatedByUserId { get; set; } + public Guid BizContactId { get; set; } + } +} diff --git a/src/Spd.Resource.Repository/ControllingMemberInvite/ControllingMemberInviteRepository.cs b/src/Spd.Resource.Repository/ControllingMemberInvite/ControllingMemberInviteRepository.cs new file mode 100644 index 000000000..9933ed283 --- /dev/null +++ b/src/Spd.Resource.Repository/ControllingMemberInvite/ControllingMemberInviteRepository.cs @@ -0,0 +1,89 @@ +using AutoMapper; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.Dynamics.CRM; +using Microsoft.Extensions.Logging; +using Spd.Utilities.Dynamics; +using Spd.Utilities.Shared; +using Spd.Utilities.Shared.Exceptions; +using System.Net; + +namespace Spd.Resource.Repository.ControllingMemberInvite +{ + internal class ControllingMemberInviteRepository : IControllingMemberInviteRepository + { + private readonly DynamicsContext _dynaContext; + private readonly IMapper _mapper; + private readonly ITimeLimitedDataProtector _dataProtector; + public ControllingMemberInviteRepository(IDynamicsContextFactory ctx, IMapper mapper, ILogger logger, IDataProtectionProvider dpProvider) + { + _dynaContext = ctx.CreateChangeOverwrite(); + _mapper = mapper; + _dataProtector = dpProvider.CreateProtector(nameof(ControllingMemberInviteCreateCmd)).ToTimeLimitedDataProtector(); + } + + public async Task> QueryAsync(ControllingMemberInviteQuery query, CancellationToken cancellationToken) + { + var invites = _dynaContext.spd_portalinvitations + .Where(i => i.statecode == DynamicsConstants.StateCode_Active) + .Where(i => i.spd_invitationtype != null && i.spd_invitationtype == (int)InvitationTypeOptionSet.ControllingMemberCRC); + + invites = invites.Where(i => i._spd_businesscontact_value == query.BizContactId); + if (!query.IncludeInactive) + invites = invites.Where(a => a.statecode != DynamicsConstants.StateCode_Inactive); + + return _mapper.Map>(invites.ToList()); + } + + public async Task ManageAsync(ControllingMemberInviteCmd cmd, CancellationToken ct) + { + if (cmd is ControllingMemberInviteCreateCmd) + await CreateControllingMemberInviteAsync((ControllingMemberInviteCreateCmd)cmd, ct); + else if (cmd is ControllingMemberInviteUpdateCmd) + await UpdateApplicationInvitesAsync((ControllingMemberInviteUpdateCmd)cmd, ct); + } + + public async Task CreateControllingMemberInviteAsync(ControllingMemberInviteCreateCmd createInviteCmd, CancellationToken ct) + { + spd_portaluser? user = await _dynaContext.GetUserById(createInviteCmd.CreatedByUserId, ct); + account? biz = await _dynaContext.GetOrgById(createInviteCmd.BizId, ct); + spd_businesscontact? bizContact = await _dynaContext.GetBizContactById(createInviteCmd.BizContactId, ct); + spd_portalinvitation invitation = _mapper.Map(createInviteCmd); + var encryptedInviteId = WebUtility.UrlEncode(_dataProtector.Protect(invitation.spd_portalinvitationid.ToString(), DateTimeOffset.UtcNow.AddDays(SpdConstants.ApplicationInviteValidDays))); + invitation.spd_invitationlink = $"{createInviteCmd.HostUrl}{SpdConstants.BizPortalControllingMemberInviteLink}{encryptedInviteId}"; + _dynaContext.AddTospd_portalinvitations(invitation); + _dynaContext.SetLink(invitation, nameof(spd_portalinvitation.spd_OrganizationId), biz); + _dynaContext.SetLink(invitation, nameof(spd_portalinvitation.spd_InvitedBy), user); + _dynaContext.SetLink(invitation, nameof(spd_portalinvitation.spd_BusinessContact), bizContact); + spd_servicetype? servicetype = _dynaContext.LookupServiceType(ServiceTypeEnum.SECURITY_BUSINESS_LICENCE_CONTROLLING_MEMBER_CRC.ToString()); + if (servicetype != null) + { + _dynaContext.SetLink(invitation, nameof(spd_portalinvitation.spd_ServiceTypeId), servicetype); + } + await _dynaContext.SaveChangesAsync(ct); + } + + private async Task UpdateApplicationInvitesAsync(ControllingMemberInviteUpdateCmd cmInviteUpdateCmd, CancellationToken cancellationToken) + { + spd_portalinvitation? invite = await _dynaContext.spd_portalinvitations + .Where(i => i.statecode == DynamicsConstants.StateCode_Active) + .Where(i => i.spd_invitationtype != null && i.spd_invitationtype == (int)InvitationTypeOptionSet.ControllingMemberCRC) + .Where(i => i.spd_portalinvitationid == cmInviteUpdateCmd.ControllingMemberInviteId) + .FirstOrDefaultAsync(cancellationToken); + if (invite == null) + throw new ApiException(HttpStatusCode.BadRequest, "Invalid invite id"); + + // Inactivate the invite + invite.statecode = DynamicsConstants.StateCode_Inactive; + invite.statuscode = (int)Enum.Parse(cmInviteUpdateCmd.ApplicationInviteStatusEnum.ToString()); + _dynaContext.UpdateObject(invite); + + await _dynaContext.SaveChangesAsync(cancellationToken); + } + + public async Task VerifyControllingMemberInvitesAsync(ControllingMemberInviteVerifyCmd verifyInviteCmd, CancellationToken ct) + { + //to be implemented + return null; + } + } +} diff --git a/src/Spd.Resource.Repository/ControllingMemberInvite/Mappings.cs b/src/Spd.Resource.Repository/ControllingMemberInvite/Mappings.cs new file mode 100644 index 000000000..d9453c9fe --- /dev/null +++ b/src/Spd.Resource.Repository/ControllingMemberInvite/Mappings.cs @@ -0,0 +1,34 @@ +using AutoMapper; +using Microsoft.Dynamics.CRM; +using Spd.Utilities.Dynamics; +using Spd.Utilities.Shared.Tools; + +namespace Spd.Resource.Repository.ControllingMemberInvite +{ + internal class Mappings : Profile + { + public Mappings() + { + _ = CreateMap() + .ForMember(d => d.spd_portalinvitationid, opt => opt.MapFrom(s => Guid.NewGuid())) + .ForMember(d => d.spd_firstname, opt => opt.MapFrom(s => StringHelper.ToTitleCase(s.GivenName))) + .ForMember(d => d.spd_surname, opt => opt.MapFrom(s => StringHelper.ToTitleCase(s.Surname))) + .ForMember(d => d.spd_email, opt => opt.MapFrom(s => s.EmailAddress)) + .ForMember(d => d.spd_invitationtype, opt => opt.MapFrom(s => InvitationTypeOptionSet.ControllingMemberCRC)) + .ForMember(d => d.spd_views, opt => opt.MapFrom(s => 0)) + .ForMember(d => d.spd_payeetype, opt => opt.MapFrom(s => PayerPreferenceOptionSet.Applicant)) + .ReverseMap() + .ForMember(d => d.GivenName, opt => opt.MapFrom(s => s.spd_firstname)) + .ForMember(d => d.Surname, opt => opt.MapFrom(s => s.spd_surname)); + + _ = CreateMap() + .IncludeBase() + .ForMember(d => d.BizId, opt => opt.MapFrom(s => s._spd_organizationid_value)) + .ForMember(d => d.Id, opt => opt.MapFrom(s => s.spd_portalinvitationid)) + .ForMember(d => d.ErrorMsg, opt => opt.MapFrom(s => s.spd_errormessage)) + .ForMember(d => d.CreatedByUserId, opt => opt.MapFrom(s => s._spd_invitedby_value)) + .ForMember(d => d.Status, opt => opt.MapFrom(s => s.statuscode == null ? ApplicationInviteStatusEnum.Draft : Enum.Parse(((InvitationStatus)s.statuscode).ToString()))) + .ForMember(d => d.Viewed, opt => opt.MapFrom(s => s.spd_views != null && s.spd_views > 0)); + } + } +} diff --git a/src/Spd.Resource.Repository/ServiceExtensions.cs b/src/Spd.Resource.Repository/ServiceExtensions.cs index 62efced22..16a7c09f9 100644 --- a/src/Spd.Resource.Repository/ServiceExtensions.cs +++ b/src/Spd.Resource.Repository/ServiceExtensions.cs @@ -9,6 +9,7 @@ using Spd.Resource.Repository.Config; using Spd.Resource.Repository.Contact; using Spd.Resource.Repository.ControllingMemberCrcApplication; +using Spd.Resource.Repository.ControllingMemberInvite; using Spd.Resource.Repository.Delegates; using Spd.Resource.Repository.Document; using Spd.Resource.Repository.DocumentTemplate; @@ -70,6 +71,7 @@ public void ConfigureServices(ConfigurationServices configurationServices) configurationServices.Services.AddTransient(); configurationServices.Services.AddTransient(); configurationServices.Services.AddTransient(); + configurationServices.Services.AddTransient(); } } } diff --git a/src/Spd.Resource.Repository/SharedContract.cs b/src/Spd.Resource.Repository/SharedContract.cs index 1a322e733..b90018530 100644 --- a/src/Spd.Resource.Repository/SharedContract.cs +++ b/src/Spd.Resource.Repository/SharedContract.cs @@ -152,4 +152,14 @@ public enum BizTypeEnum public record SwlContactInfo { public Guid? LicenceId { get; set; } +} + +public enum ApplicationInviteStatusEnum +{ + Draft, + Sent, + Failed, + Completed, + Cancelled, + Expired } \ No newline at end of file diff --git a/src/Spd.Utilities.Dynamics/Connected Services/OData Service/ConnectedService.json b/src/Spd.Utilities.Dynamics/Connected Services/OData Service/ConnectedService.json index 7975bc825..64e2f9332 100644 --- a/src/Spd.Utilities.Dynamics/Connected Services/OData Service/ConnectedService.json +++ b/src/Spd.Utilities.Dynamics/Connected Services/OData Service/ConnectedService.json @@ -28,7 +28,7 @@ "MakeTypesInternal": false, "OpenGeneratedFilesInIDE": false, "GenerateMultipleFiles": false, - "CustomHttpHeaders": "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ims0Y3BkRmdybU0yMHJhcThYVVRxMVlpaXNISSIsImtpZCI6Ims0Y3BkRmdybU0yMHJhcThYVVRxMVlpaXNISSJ9.eyJhdWQiOiJodHRwczovL3NwZC1zcGFyYy5kZXYuamFnLmdvdi5iYy5jYS9hcGkvZGF0YS92OS4wLyIsImlzcyI6Imh0dHA6Ly9zdHN0ZXN0Lmdvdi5iYy5jYS9hZGZzL3NlcnZpY2VzL3RydXN0IiwiaWF0IjoxNzI0NDM3MzYyLCJuYmYiOjE3MjQ0MzczNjIsImV4cCI6MTcyNDQ0MDk2MiwidXBuIjoic3BkX29zYWRAZ292LmJjLmNhIiwidW5pcXVlX25hbWUiOiJJRElSXFxTUERfT1NBRCIsImFwcHR5cGUiOiJDb25maWRlbnRpYWwiLCJhcHBpZCI6IjAxNzRiMjAzLWNkY2YtNDE2NC04NTMyLTBhNGM0ZmRmZGY2MiIsImF1dGhtZXRob2QiOiJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZFByb3RlY3RlZFRyYW5zcG9ydCIsImF1dGhfdGltZSI6IjIwMjQtMDgtMjNUMTg6MjI6NDIuNzA4WiIsInZlciI6IjEuMCIsInNjcCI6Im9wZW5pZCJ9.HpCzkP5zAnCgcgEIeVuJgxQn8YWtA2CMoG-7TS4pq55aVyLtM56IxXfCEqYzzLxrOfZh3Md9nhNa_XI6iBFR3-1Ft3J1zY8_iGIKJGwOAUdw0y7WzG2FQvIyYG6wNoUQKV7t47QDK8lu6wtiTGcX61PSGp3dMQipHuhwhyxjHpnna3ALb79KA8OxpjBr-vgv3Nf386ZquZ_K5hd0i_BF6gKVYIStzBz4Yh0DGuMPDzYJsjCX-2TBPah9sDYmcziq-Ok3QBKRxyugT4ntL-7uRtRCz8lmhDX1ED7sGYe_yCxtmJ0AbtNBV0dlTe_6M60rHarWQrlox2aguhAMJjrT8Q", + "CustomHttpHeaders": "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ims0Y3BkRmdybU0yMHJhcThYVVRxMVlpaXNISSIsImtpZCI6Ims0Y3BkRmdybU0yMHJhcThYVVRxMVlpaXNISSJ9.eyJhdWQiOiJodHRwczovL3NwZC1zcGFyYy5kZXYuamFnLmdvdi5iYy5jYS9hcGkvZGF0YS92OS4wLyIsImlzcyI6Imh0dHA6Ly9zdHN0ZXN0Lmdvdi5iYy5jYS9hZGZzL3NlcnZpY2VzL3RydXN0IiwiaWF0IjoxNzI0NzExNzczLCJuYmYiOjE3MjQ3MTE3NzMsImV4cCI6MTcyNDcxNTM3MywidXBuIjoic3BkX29zYWRAZ292LmJjLmNhIiwidW5pcXVlX25hbWUiOiJJRElSXFxTUERfT1NBRCIsImFwcHR5cGUiOiJDb25maWRlbnRpYWwiLCJhcHBpZCI6IjAxNzRiMjAzLWNkY2YtNDE2NC04NTMyLTBhNGM0ZmRmZGY2MiIsImF1dGhtZXRob2QiOiJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZFByb3RlY3RlZFRyYW5zcG9ydCIsImF1dGhfdGltZSI6IjIwMjQtMDgtMjZUMjI6MzY6MTMuNDYzWiIsInZlciI6IjEuMCIsInNjcCI6Im9wZW5pZCJ9.eqDSxilaKPjHWF8mdmPGjhpTGTE_fAwgFrfHcPXf-N_JOY09-ZXRnoDexUqMtSRutpNeYFEB_VcES24krXc1rsBrVoAs__od9hh39bHzX46MpHoStK9-JUgp92X2s-ugPy28vmDojA_de7bv4CoHR9ygNWBKvjdAsd4Y19mkhxbtYsQFIStnZZSVQTDf6nBtbUhidcTuVehpiZV4GN8TZzbPkgq6L8Eyk1XSiLNjUnhSUM-Qh83ECS8Rk2B6eSiCpytzTBTP_BdwK0hf7ao4XC9-5OI91hMQHWMKPDm2eNxTuYBGrn-v_9kUwsX2jV-730k2UkBUYFzsYCzT10kMLQ", "IncludeWebProxy": false, "WebProxyHost": null, "IncludeWebProxyNetworkCredentials": false, diff --git a/src/Spd.Utilities.Dynamics/Connected Services/OData Service/OData ServiceCsdl.xml b/src/Spd.Utilities.Dynamics/Connected Services/OData Service/OData ServiceCsdl.xml index 0f407021b..26a804ba5 100644 --- a/src/Spd.Utilities.Dynamics/Connected Services/OData Service/OData ServiceCsdl.xml +++ b/src/Spd.Utilities.Dynamics/Connected Services/OData Service/OData ServiceCsdl.xml @@ -28812,6 +28812,7 @@ + @@ -30502,7 +30503,7 @@ - + @@ -30511,6 +30512,7 @@ + @@ -30578,6 +30580,9 @@ + + + @@ -38754,6 +38759,7 @@ + @@ -52522,6 +52528,7 @@ + @@ -53033,6 +53040,7 @@ + diff --git a/src/Spd.Utilities.Dynamics/Connected Services/OData Service/Reference.cs b/src/Spd.Utilities.Dynamics/Connected Services/OData Service/Reference.cs index 3e6ef4646..6f10637b0 100644 --- a/src/Spd.Utilities.Dynamics/Connected Services/OData Service/Reference.cs +++ b/src/Spd.Utilities.Dynamics/Connected Services/OData Service/Reference.cs @@ -8,7 +8,7 @@ // //------------------------------------------------------------------------------ -// Generation date: 8/23/2024 11:23:10 AM +// Generation date: 8/26/2024 3:36:53 PM namespace Microsoft.Dynamics.CRM { /// @@ -14386,7 +14386,7 @@ private abstract class GeneratedEdmModel /// /// There are no comments for spd_CreateScreeningCase in the schema. /// - public virtual global::Microsoft.OData.Client.DataServiceActionQuerySingle spd_CreateScreeningCase(int Origin, string ServiceType, string FirstName, string MiddleName1, string MiddleName2, string LastName, global::System.DateTimeOffset DateOfBirth, string BirthPlace, int Sex, global::Microsoft.Dynamics.CRM.account OrganizationId, string DriverLicense, string PhoneNumber, string ApplicantPosition, string ContractedCompanyName, string EmployeeId, string AddressLine1, string AddressLine2, string City, string Province, string PostalCode, string Country, string PrimaryEmail, string EmailId, bool Declaration, global::System.DateTimeOffset DeclarationDate, bool Consent, string AliasFN1, string AliasMN1, string AliasSN1, string AliasLN1, string AliasFN2, string AliasMN2, string AliasSN2, string AliasLN2, string AliasFN3, string AliasMN3, string AliasSN3, string AliasLN3) + public virtual global::Microsoft.OData.Client.DataServiceActionQuerySingle spd_CreateScreeningCase(int Origin, string ServiceType, string FirstName, string MiddleName1, string MiddleName2, string LastName, global::System.DateTimeOffset DateOfBirth, string BirthPlace, int Sex, global::Microsoft.Dynamics.CRM.account OrganizationId, string DriverLicense, string PhoneNumber, string ApplicantPosition, string ContractedCompanyName, string EmployeeId, string AddressLine1, string AddressLine2, string City, string Province, string PostalCode, string Country, string PrimaryEmail, string EmailId, bool Declaration, global::System.DateTimeOffset DeclarationDate, bool Consent, string AliasFN1, string AliasMN1, string AliasSN1, string AliasLN1, string AliasFN2, string AliasMN2, string AliasSN2, string AliasLN2, string AliasFN3, string AliasMN3, string AliasSN3, string AliasLN3, int IdentityConfirmed) { return new global::Microsoft.OData.Client.DataServiceActionQuerySingle(this, this.BaseUri.OriginalString.Trim('/') + "/spd_CreateScreeningCase", new global::Microsoft.OData.Client.BodyOperationParameter("Origin", Origin), new global::Microsoft.OData.Client.BodyOperationParameter("ServiceType", ServiceType), @@ -14425,7 +14425,8 @@ private abstract class GeneratedEdmModel new global::Microsoft.OData.Client.BodyOperationParameter("AliasFN3", AliasFN3), new global::Microsoft.OData.Client.BodyOperationParameter("AliasMN3", AliasMN3), new global::Microsoft.OData.Client.BodyOperationParameter("AliasSN3", AliasSN3), - new global::Microsoft.OData.Client.BodyOperationParameter("AliasLN3", AliasLN3)); + new global::Microsoft.OData.Client.BodyOperationParameter("AliasLN3", AliasLN3), + new global::Microsoft.OData.Client.BodyOperationParameter("IdentityConfirmed", IdentityConfirmed)); } /// /// There are no comments for spd_CreateUnresolvedEmail in the schema. @@ -664532,6 +664533,27 @@ public spd_businesscontactSingle(global::Microsoft.OData.Client.DataServiceQuery } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.OData.Client.Design.T4", "#VersionNumber#")] private global::Microsoft.OData.Client.DataServiceQuery _spd_position_spd_businesscontact; + /// + /// There are no comments for spd_businesscontact_spd_portalinvitation in the schema. + /// + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.OData.Client.Design.T4", "#VersionNumber#")] + public virtual global::Microsoft.OData.Client.DataServiceQuery spd_businesscontact_spd_portalinvitation + { + get + { + if (!this.IsComposable) + { + throw new global::System.NotSupportedException("The previous function is not composable."); + } + if ((this._spd_businesscontact_spd_portalinvitation == null)) + { + this._spd_businesscontact_spd_portalinvitation = Context.CreateQuery(GetPath("spd_businesscontact_spd_portalinvitation")); + } + return this._spd_businesscontact_spd_portalinvitation; + } + } + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.OData.Client.Design.T4", "#VersionNumber#")] + private global::Microsoft.OData.Client.DataServiceQuery _spd_businesscontact_spd_portalinvitation; } /// /// There are no comments for spd_businesscontact in the schema. @@ -665674,6 +665696,28 @@ public virtual string spd_firstname partial void Onspd_position_spd_businesscontactChanging(global::Microsoft.OData.Client.DataServiceCollection value); partial void Onspd_position_spd_businesscontactChanged(); /// + /// There are no comments for Property spd_businesscontact_spd_portalinvitation in the schema. + /// + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.OData.Client.Design.T4", "#VersionNumber#")] + public virtual global::Microsoft.OData.Client.DataServiceCollection spd_businesscontact_spd_portalinvitation + { + get + { + return this._spd_businesscontact_spd_portalinvitation; + } + set + { + this.Onspd_businesscontact_spd_portalinvitationChanging(value); + this._spd_businesscontact_spd_portalinvitation = value; + this.Onspd_businesscontact_spd_portalinvitationChanged(); + this.OnPropertyChanged("spd_businesscontact_spd_portalinvitation"); + } + } + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.OData.Client.Design.T4", "#VersionNumber#")] + private global::Microsoft.OData.Client.DataServiceCollection _spd_businesscontact_spd_portalinvitation = new global::Microsoft.OData.Client.DataServiceCollection(null, global::Microsoft.OData.Client.TrackingMode.None); + partial void Onspd_businesscontact_spd_portalinvitationChanging(global::Microsoft.OData.Client.DataServiceCollection value); + partial void Onspd_businesscontact_spd_portalinvitationChanged(); + /// /// There are no comments for spd_GetBusinessContactTitle in the schema. /// public virtual global::Microsoft.OData.Client.DataServiceActionQuerySingle spd_GetBusinessContactTitle() @@ -703541,6 +703585,27 @@ public spd_portalinvitationSingle(global::Microsoft.OData.Client.DataServiceQuer } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.OData.Client.Design.T4", "#VersionNumber#")] private global::Microsoft.Dynamics.CRM.spd_portaluserSingle _spd_InvitedBy; + /// + /// There are no comments for spd_BusinessContact in the schema. + /// + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.OData.Client.Design.T4", "#VersionNumber#")] + public virtual global::Microsoft.Dynamics.CRM.spd_businesscontactSingle spd_BusinessContact + { + get + { + if (!this.IsComposable) + { + throw new global::System.NotSupportedException("The previous function is not composable."); + } + if ((this._spd_BusinessContact == null)) + { + this._spd_BusinessContact = new global::Microsoft.Dynamics.CRM.spd_businesscontactSingle(this.Context, GetPath("spd_BusinessContact")); + } + return this._spd_BusinessContact; + } + } + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.OData.Client.Design.T4", "#VersionNumber#")] + private global::Microsoft.Dynamics.CRM.spd_businesscontactSingle _spd_BusinessContact; } /// /// There are no comments for spd_portalinvitation in the schema. @@ -703565,6 +703630,7 @@ public partial class spd_portalinvitation : crmbaseentity /// Initial value of spd_Applicant. /// Initial value of spd_ServiceTypeId. /// Initial value of spd_InvitedBy. + /// Initial value of spd_BusinessContact. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.OData.Client.Design.T4", "#VersionNumber#")] public static spd_portalinvitation Createspd_portalinvitation(global::Microsoft.Dynamics.CRM.systemuser createdby, global::Microsoft.Dynamics.CRM.systemuser createdonbehalfby, @@ -703575,7 +703641,8 @@ public static spd_portalinvitation Createspd_portalinvitation(global::Microsoft. global::Microsoft.Dynamics.CRM.account spd_OrganizationId, global::Microsoft.Dynamics.CRM.contact spd_Applicant, global::Microsoft.Dynamics.CRM.spd_servicetype spd_ServiceTypeId, - global::Microsoft.Dynamics.CRM.spd_portaluser spd_InvitedBy) + global::Microsoft.Dynamics.CRM.spd_portaluser spd_InvitedBy, + global::Microsoft.Dynamics.CRM.spd_businesscontact spd_BusinessContact) { spd_portalinvitation spd_portalinvitation = new spd_portalinvitation(); if ((createdby == null)) @@ -703628,6 +703695,11 @@ public static spd_portalinvitation Createspd_portalinvitation(global::Microsoft. throw new global::System.ArgumentNullException("spd_InvitedBy"); } spd_portalinvitation.spd_InvitedBy = spd_InvitedBy; + if ((spd_BusinessContact == null)) + { + throw new global::System.ArgumentNullException("spd_BusinessContact"); + } + spd_portalinvitation.spd_BusinessContact = spd_BusinessContact; return spd_portalinvitation; } /// @@ -703697,27 +703769,27 @@ public static spd_portalinvitation Createspd_portalinvitation(global::Microsoft. partial void OnstatecodeChanging(global::System.Nullable value); partial void OnstatecodeChanged(); /// - /// There are no comments for Property _createdby_value in the schema. + /// There are no comments for Property _spd_businesscontact_value in the schema. /// [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.OData.Client.Design.T4", "#VersionNumber#")] - public virtual global::System.Nullable _createdby_value + public virtual global::System.Nullable _spd_businesscontact_value { get { - return this.__createdby_value; + return this.__spd_businesscontact_value; } set { - this.On_createdby_valueChanging(value); - this.__createdby_value = value; - this.On_createdby_valueChanged(); - this.OnPropertyChanged("_createdby_value"); + this.On_spd_businesscontact_valueChanging(value); + this.__spd_businesscontact_value = value; + this.On_spd_businesscontact_valueChanged(); + this.OnPropertyChanged("_spd_businesscontact_value"); } } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.OData.Client.Design.T4", "#VersionNumber#")] - private global::System.Nullable __createdby_value; - partial void On_createdby_valueChanging(global::System.Nullable value); - partial void On_createdby_valueChanged(); + private global::System.Nullable __spd_businesscontact_value; + partial void On_spd_businesscontact_valueChanging(global::System.Nullable value); + partial void On_spd_businesscontact_valueChanged(); /// /// There are no comments for Property _spd_servicetypeid_value in the schema. /// @@ -703895,6 +703967,28 @@ public virtual string spd_surname partial void On_organizationid_valueChanging(global::System.Nullable value); partial void On_organizationid_valueChanged(); /// + /// There are no comments for Property _createdby_value in the schema. + /// + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.OData.Client.Design.T4", "#VersionNumber#")] + public virtual global::System.Nullable _createdby_value + { + get + { + return this.__createdby_value; + } + set + { + this.On_createdby_valueChanging(value); + this.__createdby_value = value; + this.On_createdby_valueChanged(); + this.OnPropertyChanged("_createdby_value"); + } + } + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.OData.Client.Design.T4", "#VersionNumber#")] + private global::System.Nullable __createdby_value; + partial void On_createdby_valueChanging(global::System.Nullable value); + partial void On_createdby_valueChanged(); + /// /// There are no comments for Property _spd_organizationid_value in the schema. /// [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.OData.Client.Design.T4", "#VersionNumber#")] @@ -704938,6 +705032,29 @@ public virtual string spd_email private global::Microsoft.Dynamics.CRM.spd_portaluser _spd_InvitedBy; partial void Onspd_InvitedByChanging(global::Microsoft.Dynamics.CRM.spd_portaluser value); partial void Onspd_InvitedByChanged(); + /// + /// There are no comments for Property spd_BusinessContact in the schema. + /// + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.OData.Client.Design.T4", "#VersionNumber#")] + [global::System.ComponentModel.DataAnnotations.RequiredAttribute(ErrorMessage = "spd_BusinessContact is required.")] + public virtual global::Microsoft.Dynamics.CRM.spd_businesscontact spd_BusinessContact + { + get + { + return this._spd_BusinessContact; + } + set + { + this.Onspd_BusinessContactChanging(value); + this._spd_BusinessContact = value; + this.Onspd_BusinessContactChanged(); + this.OnPropertyChanged("spd_BusinessContact"); + } + } + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.OData.Client.Design.T4", "#VersionNumber#")] + private global::Microsoft.Dynamics.CRM.spd_businesscontact _spd_BusinessContact; + partial void Onspd_BusinessContactChanging(global::Microsoft.Dynamics.CRM.spd_businesscontact value); + partial void Onspd_BusinessContactChanged(); } /// /// There are no comments for spd_portaluserSingle in the schema. diff --git a/src/Spd.Utilities.Dynamics/DynamicsContextLookupHelpers.cs b/src/Spd.Utilities.Dynamics/DynamicsContextLookupHelpers.cs index 134767152..53a85c6a7 100644 --- a/src/Spd.Utilities.Dynamics/DynamicsContextLookupHelpers.cs +++ b/src/Spd.Utilities.Dynamics/DynamicsContextLookupHelpers.cs @@ -348,6 +348,21 @@ public static string LookupLicenceCategoryKey(Guid? licenceCategoryId) } + public static async Task GetBizContactById(this DynamicsContext context, Guid bizContactId, CancellationToken ct) + { + try + { + return await context.spd_businesscontacts.Where(a => a.spd_businesscontactid == bizContactId).SingleOrDefaultAsync(ct); + } + catch (DataServiceQueryException ex) + { + if (ex.Response.StatusCode == 404) + return null; + else + throw; + } + } + public static async Task GetTaskById(this DynamicsContext context, Guid taskId, CancellationToken ct) => await context.tasks.Where(a => a.activityid == taskId) .Where(a => a.statecode != DynamicsConstants.StateCode_Inactive) diff --git a/src/Spd.Utilities.Dynamics/OptionSets.cs b/src/Spd.Utilities.Dynamics/OptionSets.cs index 4fc47eb6a..6d1267499 100644 --- a/src/Spd.Utilities.Dynamics/OptionSets.cs +++ b/src/Spd.Utilities.Dynamics/OptionSets.cs @@ -141,6 +141,7 @@ public enum InvitationTypeOptionSet { PortalUser = 100000000, ScreeningRequest = 100000001, + ControllingMemberCRC = 100000002 } public enum InvitationStatus diff --git a/src/Spd.Utilities.Shared/SpdConstants.cs b/src/Spd.Utilities.Shared/SpdConstants.cs index f3450601c..df2bb9060 100644 --- a/src/Spd.Utilities.Shared/SpdConstants.cs +++ b/src/Spd.Utilities.Shared/SpdConstants.cs @@ -12,6 +12,7 @@ public static class SpdConstants public static readonly string DefaultBannerMsg = "10 business days for online applications and 20 business days for manual applications."; public static readonly string UserInviteLink = "crrp/invitation/"; public static readonly string BizPortalUserInviteLink = "business-licence/invitation/"; + public static readonly string BizPortalControllingMemberInviteLink = "business-licence/cm-crc-invitation/"; public static readonly string CrrpApplicationInviteLink = "crrpa/invitation/"; public static readonly string PssoApplicationInviteLink = "pssoa/invitation/"; public static readonly int ShareableClearanceExpiredDateBufferInMonths = 6;