-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathGrantApplicationAppService.cs
More file actions
982 lines (861 loc) · 43.4 KB
/
GrantApplicationAppService.cs
File metadata and controls
982 lines (861 loc) · 43.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Unity.Flex.WorksheetInstances;
using Unity.Flex.Worksheets;
using Unity.GrantManager.ApplicationForms;
using Unity.GrantManager.Applications;
using Unity.GrantManager.Comments;
using Unity.GrantManager.Events;
using Unity.GrantManager.Exceptions;
using Unity.GrantManager.Flex;
using Unity.GrantManager.Identity;
using Unity.GrantManager.Payments;
using Unity.GrantManager.Permissions;
using Unity.GrantManager.Zones;
using Unity.Modules.Shared;
using Unity.Modules.Shared.Correlation;
using Unity.Payments.Domain.PaymentRequests;
using Unity.Payments.Enums;
using Unity.Payments.Integrations.Cas;
using Unity.Payments.PaymentRequests;
using Volo.Abp;
using Volo.Abp.Application.Dtos;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Entities;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.EventBus.Local;
namespace Unity.GrantManager.GrantApplications;
[Authorize]
[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(GrantApplicationAppService), typeof(IGrantApplicationAppService))]
public class GrantApplicationAppService : GrantManagerAppService, IGrantApplicationAppService
{
private readonly IApplicationRepository _applicationRepository;
private readonly IApplicationManager _applicationManager;
private readonly IApplicationStatusRepository _applicationStatusRepository;
private readonly IApplicationFormSubmissionRepository _applicationFormSubmissionRepository;
private readonly IApplicationAssignmentRepository _applicationAssignmentRepository;
private readonly IApplicantRepository _applicantRepository;
private readonly ICommentsManager _commentsManager;
private readonly IApplicationFormRepository _applicationFormRepository;
private readonly IPersonRepository _personRepository;
private readonly IApplicantAgentRepository _applicantAgentRepository;
private readonly IApplicantAddressRepository _applicantAddressRepository;
private readonly ILocalEventBus _localEventBus;
private readonly ISupplierService _iSupplierService;
private readonly IPaymentRequestAppService _paymentRequestService;
private readonly IPaymentRequestRepository _paymentRequestsRepository;
private readonly IZoneChecker _zoneChecker;
public GrantApplicationAppService(
IApplicationManager applicationManager,
IApplicationRepository applicationRepository,
IApplicationStatusRepository applicationStatusRepository,
IApplicationAssignmentRepository applicationAssignmentRepository,
IApplicationFormSubmissionRepository applicationFormSubmissionRepository,
IApplicantRepository applicantRepository,
ICommentsManager commentsManager,
IApplicationFormRepository applicationFormRepository,
IPersonRepository personRepository,
IApplicantAgentRepository applicantAgentRepository,
IApplicantAddressRepository applicantAddressRepository,
ILocalEventBus localEventBus,
ISupplierService iSupplierService,
IPaymentRequestAppService paymentRequestService,
IPaymentRequestRepository paymentRequestsRepository,
IZoneChecker zoneChecker)
{
_applicationRepository = applicationRepository;
_applicationManager = applicationManager;
_applicationStatusRepository = applicationStatusRepository;
_applicationAssignmentRepository = applicationAssignmentRepository;
_applicationFormSubmissionRepository = applicationFormSubmissionRepository;
_applicantRepository = applicantRepository;
_commentsManager = commentsManager;
_applicationFormRepository = applicationFormRepository;
_personRepository = personRepository;
_applicantAgentRepository = applicantAgentRepository;
_applicantAddressRepository = applicantAddressRepository;
_iSupplierService = iSupplierService;
_localEventBus = localEventBus;
_paymentRequestService = paymentRequestService;
_paymentRequestsRepository = paymentRequestsRepository;
_zoneChecker = zoneChecker;
}
public async Task<PagedResultDto<GrantApplicationDto>> GetListAsync(PagedAndSortedResultRequestDto input)
{
var groupedResult = await _applicationRepository.WithFullDetailsGroupedAsync(input.SkipCount, input.MaxResultCount, input.Sorting);
// Pre-fetch payment requests for all applications in a single query to reduce database calls
var applicationIds = groupedResult.SelectMany(g => g).Select(a => a.Id).ToList();
bool paymentsFeatureEnabled = await FeatureChecker.IsEnabledAsync(PaymentConsts.UnityPaymentsFeature);
List<PaymentDetailsDto> paymentRequests = new List<PaymentDetailsDto>();
if (paymentsFeatureEnabled)
{
paymentRequests = await _paymentRequestService.GetListByApplicationIdsAsync(applicationIds);
}
// Map applications to DTOs
var appDtos = groupedResult.Select(grouping =>
{
var firstApplication = grouping.First();
var appDto = ObjectMapper.Map<Application, GrantApplicationDto>(firstApplication);
// Map additional properties
appDto.Status = firstApplication.ApplicationStatus.InternalStatus;
appDto.Applicant = ObjectMapper.Map<Applicant, GrantApplicationApplicantDto>(firstApplication.Applicant);
appDto.Category = firstApplication.ApplicationForm.Category ?? string.Empty;
appDto.ApplicationTag = firstApplication.ApplicationTags?.FirstOrDefault()?.Text ?? string.Empty;
appDto.Owner = BuildApplicationOwner(firstApplication.Owner);
appDto.OrganizationName = firstApplication.Applicant?.OrgName ?? string.Empty;
appDto.OrganizationType = firstApplication.Applicant?.OrganizationType ?? string.Empty;
appDto.Assignees = BuildApplicationAssignees(firstApplication.ApplicationAssignments);
appDto.SubStatusDisplayValue = MapSubstatusDisplayValue(appDto.SubStatus);
appDto.DeclineRational = MapDeclineRationalDisplayValue(appDto.DeclineRational);
appDto.ContactFullName = firstApplication.ApplicantAgent?.Name;
appDto.ContactEmail = firstApplication.ApplicantAgent?.Email;
appDto.ContactTitle = firstApplication.ApplicantAgent?.Title;
appDto.ContactBusinessPhone = firstApplication.ApplicantAgent?.Phone;
appDto.ContactCellPhone = firstApplication.ApplicantAgent?.Phone2;
//Payment request info if the feature is enabled
if (paymentsFeatureEnabled && paymentRequests != null && paymentRequests is { Count: > 0 })
{
var paymentInfo = new PaymentInfoDto
{
ApprovedAmount = firstApplication.ApprovedAmount,
TotalPaid = paymentRequests
.Where(pr => pr.CorrelationId == firstApplication.Id && pr.Status.Equals(PaymentRequestStatus.Submitted))
.Sum(pr => pr.Amount)
};
appDto.PaymentInfo = paymentInfo;
}
return appDto;
}).ToList();
var totalCount = await _applicationRepository.GetCountAsync();
return new PagedResultDto<GrantApplicationDto>(totalCount, appDtos);
}
private static string MapSubstatusDisplayValue(string subStatus)
{
if (subStatus == null) { return string.Empty; }
var hasKey = AssessmentResultsOptionsList.SubStatusActionList.TryGetValue(subStatus, out string? subStatusValue);
if (hasKey)
return subStatusValue ?? string.Empty;
else
return string.Empty;
}
private static string MapDeclineRationalDisplayValue(string value)
{
if (value == null) { return string.Empty; }
var hasKey = AssessmentResultsOptionsList.DeclineRationalActionList.TryGetValue(value, out string? subStatusValue);
if (hasKey)
return subStatusValue ?? string.Empty;
else
return string.Empty;
}
private static List<GrantApplicationAssigneeDto> BuildApplicationAssignees(IEnumerable<ApplicationAssignment>? applicationAssignments)
{
var appAssignmentDtos = new List<GrantApplicationAssigneeDto>();
if (applicationAssignments != null)
{
foreach (var assignment in applicationAssignments)
{
appAssignmentDtos.Add(new GrantApplicationAssigneeDto()
{
ApplicationId = assignment.ApplicationId,
AssigneeId = assignment.AssigneeId,
FullName = assignment.Assignee?.FullName ?? string.Empty,
Id = assignment.Id,
Duty = assignment.Duty
});
}
}
return appAssignmentDtos;
}
private static GrantApplicationAssigneeDto BuildApplicationOwner(Person? applicationOwner)
{
if (applicationOwner != null)
{
return new GrantApplicationAssigneeDto()
{
Id = applicationOwner.Id,
FullName = applicationOwner.FullName
};
}
return new GrantApplicationAssigneeDto();
}
public async Task<GrantApplicationDto> GetAsync(Guid id)
{
var application = await _applicationRepository.GetWithFullDetailsByIdAsync(id);
if (application == null) return new GrantApplicationDto();
var appDto = ObjectMapper.Map<Application, GrantApplicationDto>(application);
appDto.StatusCode = application.ApplicationStatus.StatusCode;
appDto.Status = application.ApplicationStatus.InternalStatus;
if (application.ApplicantAgent != null)
{
appDto.ContactFullName = application.ApplicantAgent.Name;
appDto.ContactEmail = application.ApplicantAgent.Email;
appDto.ContactTitle = application.ApplicantAgent.Title;
appDto.ContactBusinessPhone = application.ApplicantAgent.Phone;
appDto.ContactCellPhone = application.ApplicantAgent.Phone2;
}
if (application.Applicant != null)
{
appDto.OrganizationName = application.Applicant.OrgName;
appDto.OrgNumber = application.Applicant.OrgNumber;
appDto.OrganizationSize = application.Applicant.OrganizationSize;
appDto.OrgStatus = application.Applicant.OrgStatus;
appDto.OrganizationName = application.Applicant.OrgName;
appDto.Sector = application.Applicant.Sector;
appDto.OrganizationType = application.Applicant.OrganizationType;
appDto.SubSector = application.Applicant.SubSector;
appDto.SectorSubSectorIndustryDesc = application.Applicant.SectorSubSectorIndustryDesc;
}
return appDto;
}
public async Task<ApplicationForm?> GetApplicationFormAsync(Guid applicationFormId)
{
return await (await _applicationFormRepository.GetQueryableAsync()).FirstOrDefaultAsync(s => s.Id == applicationFormId);
}
public async Task<GetSummaryDto> GetSummaryAsync(Guid applicationId)
{
var query = from application in await _applicationRepository.GetQueryableAsync()
join applicationForm in await _applicationFormRepository.GetQueryableAsync() on application.ApplicationFormId equals applicationForm.Id
join applicant in await _applicantRepository.GetQueryableAsync() on application.ApplicantId equals applicant.Id
where application.Id == applicationId
select new GetSummaryDto
{
Category = applicationForm == null ? string.Empty : applicationForm.Category,
SubmissionDate = application.SubmissionDate,
OrganizationName = applicant.OrgName,
OrganizationNumber = applicant.OrgNumber,
EconomicRegion = application.EconomicRegion,
City = application.City,
RequestedAmount = application.RequestedAmount,
ProjectBudget = application.TotalProjectBudget,
Sector = applicant.Sector,
Community = application.Community,
Status = application.ApplicationStatus.InternalStatus,
LikelihoodOfFunding = application.LikelihoodOfFunding != null && application.LikelihoodOfFunding != "" ? AssessmentResultsOptionsList.FundingList[application.LikelihoodOfFunding] : "",
AssessmentStartDate = string.Format("{0:yyyy/MM/dd}", application.AssessmentStartDate),
FinalDecisionDate = string.Format("{0:yyyy/MM/dd}", application.FinalDecisionDate),
TotalScore = application.TotalScore.ToString(),
AssessmentResult = application.AssessmentResultStatus != null && application.AssessmentResultStatus != "" ? AssessmentResultsOptionsList.AssessmentResultStatusList[application.AssessmentResultStatus] : "",
RecommendedAmount = application.RecommendedAmount,
ApprovedAmount = application.ApprovedAmount,
Batch = "", // to-do: ask BA for the implementation of Batch field,
RegionalDistrict = application.RegionalDistrict,
OwnerId = application.OwnerId,
};
var queryResult = await AsyncExecuter.FirstOrDefaultAsync(query);
if (queryResult != null)
{
var ownerId = queryResult.OwnerId ?? Guid.Empty;
queryResult.Owner = await GetOwnerAsync(ownerId);
queryResult.Assignees = await GetAssigneesAsync(applicationId);
return queryResult;
}
else
{
return await Task.FromResult(new GetSummaryDto());
}
}
[Authorize(UnitySelector.Review.AssessmentResults.Update.Default)]
public async Task<GrantApplicationDto> UpdateAssessmentResultsAsync(Guid id, CreateUpdateAssessmentResultsDto input)
{
var application = await _applicationRepository.GetAsync(id);
await SanitizeApprovalZoneInputs(input, application);
await SanitizeAssessmentResultsZoneInputs(input, application);
application.ValidateAndChangeDueDate(input.DueDate);
application.UpdateAlwaysChangeableFields(input.Notes, input.SubStatus, input.LikelihoodOfFunding, input.TotalProjectBudget, input.NotificationDate, input.RiskRanking);
if (application.IsInFinalDecisionState())
{
if (await AuthorizationService.IsGrantedAsync(UnitySelector.Review.Approval.Update.UpdateFinalStateFields))
{
application.UpdateApprovalFieldsRequiringPostEditPermission(input.ApprovedAmount);
}
if (await AuthorizationService.IsGrantedAsync(UnitySelector.Review.AssessmentResults.Update.UpdateFinalStateFields)) // User allowed to edit specific fields past approval
{
application.UpdateAssessmentResultFieldsRequiringPostEditPermission(input.RequestedAmount, input.TotalScore);
}
}
else
{
if (await CurrentUserCanUpdateAssessmentFieldsAsync())
{
application.ValidateAndChangeFinalDecisionDate(input.FinalDecisionDate);
application.UpdateApprovalFieldsRequiringPostEditPermission(input.ApprovedAmount);
application.UpdateAssessmentResultFieldsRequiringPostEditPermission(input.RequestedAmount, input.TotalScore);
application.UpdateFieldsOnlyForPreFinalDecision(input.DueDiligenceStatus,
input.RecommendedAmount,
input.DeclineRational);
application.UpdateAssessmentResultStatus(input.AssessmentResultStatus);
}
}
await PublishCustomFieldUpdatesAsync(application.Id, FlexConsts.AssessmentInfoUiAnchor, input);
await _applicationRepository.UpdateAsync(application);
return ObjectMapper.Map<Application, GrantApplicationDto>(application);
}
private async Task SanitizeApprovalZoneInputs(CreateUpdateAssessmentResultsDto input, Application application)
{
// Approval Zone Fields - Disabled Inputs
input.ApprovedAmount ??= application.ApprovedAmount;
// Sanitize if zone is disabled
if (!await _zoneChecker.IsEnabledAsync(UnitySelector.Review.Approval.Default, application.ApplicationFormId))
{
input.SubStatus ??= application.SubStatus;
input.FinalDecisionDate ??= application.FinalDecisionDate;
input.Notes ??= application.Notes;
}
else
{
// Sanitize if zone is enabled but fields are disabled
if (application.IsInFinalDecisionState())
{
input.FinalDecisionDate ??= application.FinalDecisionDate;
}
}
}
private async Task SanitizeAssessmentResultsZoneInputs(CreateUpdateAssessmentResultsDto input, Application application)
{
// Approval Zone Fields - Disabled Inputs
input.RequestedAmount ??= application.RequestedAmount;
input.TotalProjectBudget ??= application.TotalProjectBudget;
input.RecommendedAmount ??= application.RecommendedAmount;
input.TotalScore ??= application.TotalScore;
// Sanitize if zone is disabled
if (!await _zoneChecker.IsEnabledAsync(UnitySelector.Review.AssessmentResults.Default, application.ApplicationFormId))
{
input.LikelihoodOfFunding ??= application.LikelihoodOfFunding;
input.RiskRanking ??= application.RiskRanking;
input.DueDiligenceStatus ??= application.DueDiligenceStatus;
input.AssessmentResultStatus ??= application.AssessmentResultStatus;
input.DeclineRational ??= application.DeclineRational;
input.NotificationDate ??= application.NotificationDate;
input.DueDate ??= application.DueDate;
}
else
{
// Sanitize if zone is enabled but fields are disabled
if (application.IsInFinalDecisionState())
{
input.LikelihoodOfFunding ??= application.LikelihoodOfFunding;
input.RiskRanking ??= application.RiskRanking;
input.DueDiligenceStatus ??= application.DueDiligenceStatus;
input.AssessmentResultStatus ??= application.AssessmentResultStatus;
input.DeclineRational ??= application.DeclineRational;
}
}
}
private async Task<bool> CurrentUserCanUpdateAssessmentFieldsAsync()
{
return await AuthorizationService.IsGrantedAsync(UnitySelector.Review.AssessmentResults.Update.Default);
}
[Authorize(GrantApplicationPermissions.ProjectInfo.Update)]
public async Task<GrantApplicationDto> UpdateProjectInfoAsync(Guid id, CreateUpdateProjectInfoDto input)
{
var application = await _applicationRepository.GetAsync(id);
SanitizeProjectInfoDisabledInputs(input, application);
var percentageTotalProjectBudget = (input.TotalProjectBudget == 0 || input.TotalProjectBudget == null) ? 0 : decimal.Multiply(decimal.Divide(input.RequestedAmount ?? 0, input.TotalProjectBudget ?? 0), 100).To<double>();
if (application != null)
{
application.ProjectSummary = input.ProjectSummary;
application.ProjectName = input.ProjectName ?? string.Empty;
application.RequestedAmount = input.RequestedAmount ?? 0;
application.TotalProjectBudget = input.TotalProjectBudget ?? 0;
application.ProjectStartDate = input.ProjectStartDate;
application.ProjectEndDate = input.ProjectEndDate;
application.PercentageTotalProjectBudget = Math.Round(percentageTotalProjectBudget, 2);
application.ProjectFundingTotal = input.ProjectFundingTotal;
application.Community = input.Community;
application.CommunityPopulation = input.CommunityPopulation;
application.Acquisition = input.Acquisition;
application.Forestry = input.Forestry;
application.ForestryFocus = input.ForestryFocus;
application.EconomicRegion = input.EconomicRegion;
application.ElectoralDistrict = input.ElectoralDistrict;
application.RegionalDistrict = input.RegionalDistrict;
application.Place = input.Place;
await PublishCustomFieldUpdatesAsync(application.Id, FlexConsts.ProjectInfoUiAnchor, input);
await _applicationRepository.UpdateAsync(application);
return ObjectMapper.Map<Application, GrantApplicationDto>(application);
}
else
{
throw new EntityNotFoundException();
}
}
public async Task<GrantApplicationDto> UpdateFundingAgreementInfoAsync(Guid id, CreateUpdateFundingAgreementInfoDto input)
{
var application = await _applicationRepository.GetAsync(id);
if (application != null)
{
application.ContractNumber = input.ContractNumber;
application.ContractExecutionDate = input.ContractExecutionDate;
await PublishCustomFieldUpdatesAsync(application.Id, FlexConsts.FundingAgreementInfoUiAnchor, input);
await _applicationRepository.UpdateAsync(application);
return ObjectMapper.Map<Application, GrantApplicationDto>(application);
}
else
{
throw new EntityNotFoundException();
}
}
private static void SanitizeProjectInfoDisabledInputs(CreateUpdateProjectInfoDto input, Application application)
{
// Cater for disabled fields that are not serialized with post - fall back to the previous value, these should be 0 from the API call
input.TotalProjectBudget ??= application.TotalProjectBudget;
input.RequestedAmount ??= application.RequestedAmount;
input.ProjectFundingTotal ??= application.ProjectFundingTotal;
}
[Authorize(GrantApplicationPermissions.ApplicantInfo.Update)]
public async Task<GrantApplicationDto> UpdateProjectApplicantInfoAsync(Guid id, CreateUpdateApplicantInfoDto input)
{
var application = await _applicationRepository.GetAsync(id);
if (application != null)
{
var applicant = await _applicantRepository.FirstOrDefaultAsync(a => a.Id == application.ApplicantId) ?? throw new EntityNotFoundException();
// This applicant should never be null!
applicant.OrganizationType = input.OrganizationType ?? "";
applicant.OrgName = input.OrgName ?? "";
applicant.OrgNumber = input.OrgNumber ?? "";
applicant.OrgStatus = input.OrgStatus ?? "";
applicant.OrganizationSize = input.OrganizationSize ?? "";
applicant.Sector = input.Sector ?? "";
applicant.SubSector = input.SubSector ?? "";
applicant.SectorSubSectorIndustryDesc = input.SectorSubSectorIndustryDesc ?? "";
applicant.IndigenousOrgInd = input.IndigenousOrgInd ?? "";
applicant.UnityApplicantId = input.UnityApplicantId ?? "";
applicant.FiscalDay = input.FiscalDay;
applicant.FiscalMonth = input.FiscalMonth ?? "";
applicant.NonRegOrgName = input.NonRegOrgName ?? "";
_ = await _applicantRepository.UpdateAsync(applicant);
// Integrate with payments module to update / insert supplier
// Check that the original supplier number has changed
if (await FeatureChecker.IsEnabledAsync(PaymentConsts.UnityPaymentsFeature)
&& !string.IsNullOrEmpty(input.SupplierNumber)
&& input.OriginalSupplierNumber != input.SupplierNumber)
{
var pendingPayments = await _paymentRequestsRepository.GetPaymentPendingListByCorrelationIdAsync(id);
if (pendingPayments != null && pendingPayments.Count > 0)
{
throw new UserFriendlyException("There are outstanding payment requests with the current Supplier. Please decline or approve the outstanding payments before changing the Supplier Number");
}
await _iSupplierService.UpdateApplicantSupplierInfo(input.SupplierNumber, application.ApplicantId);
}
var applicantAgent = await _applicantAgentRepository.FirstOrDefaultAsync(agent => agent.ApplicantId == application.ApplicantId);
if (applicantAgent == null)
{
applicantAgent = await _applicantAgentRepository.InsertAsync(new ApplicantAgent
{
ApplicantId = application.ApplicantId,
ApplicationId = application.Id,
Name = input.ContactFullName ?? "",
Phone = input.ContactBusinessPhone ?? "",
Phone2 = input.ContactCellPhone ?? "",
Email = input.ContactEmail ?? "",
Title = input.ContactTitle ?? ""
});
}
else
{
applicantAgent.Name = input.ContactFullName ?? "";
applicantAgent.Phone = input.ContactBusinessPhone ?? "";
applicantAgent.Phone2 = input.ContactCellPhone ?? "";
applicantAgent.Email = input.ContactEmail ?? "";
applicantAgent.Title = input.ContactTitle ?? "";
applicantAgent = await _applicantAgentRepository.UpdateAsync(applicantAgent);
}
await UpdateApplicantAddresses(input);
application.SigningAuthorityFullName = input.SigningAuthorityFullName ?? "";
application.SigningAuthorityTitle = input.SigningAuthorityTitle ?? "";
application.SigningAuthorityEmail = input.SigningAuthorityEmail ?? "";
application.SigningAuthorityBusinessPhone = input.SigningAuthorityBusinessPhone ?? "";
application.SigningAuthorityCellPhone = input.SigningAuthorityCellPhone ?? "";
await PublishCustomFieldUpdatesAsync(application.Id, FlexConsts.ApplicantInfoUiAnchor, input);
await _applicationRepository.UpdateAsync(application);
var appDto = ObjectMapper.Map<Application, GrantApplicationDto>(application);
appDto.ContactFullName = applicantAgent.Name;
appDto.ContactEmail = applicantAgent.Email;
appDto.ContactTitle = applicantAgent.Title;
appDto.ContactBusinessPhone = applicantAgent.Phone;
appDto.ContactCellPhone = applicantAgent.Phone2;
return appDto;
}
else
{
throw new EntityNotFoundException();
}
}
protected virtual async Task PublishCustomFieldUpdatesAsync(Guid applicationId,
string uiAnchor,
CustomDataFieldDto input)
{
if (await FeatureChecker.IsEnabledAsync("Unity.Flex"))
{
if (input.CorrelationId != Guid.Empty)
{
await _localEventBus.PublishAsync(new PersistWorksheetIntanceValuesEto()
{
InstanceCorrelationId = applicationId,
InstanceCorrelationProvider = CorrelationConsts.Application,
SheetCorrelationId = input.CorrelationId,
SheetCorrelationProvider = CorrelationConsts.FormVersion,
UiAnchor = uiAnchor,
CustomFields = input.CustomFields,
WorksheetId = input.WorksheetId
});
}
else
{
Logger.LogError("Unable to resolve for version");
}
}
}
protected virtual async Task UpdateApplicantAddresses(CreateUpdateApplicantInfoDto input)
{
List<ApplicantAddress> applicantAddresses = await _applicantAddressRepository.FindByApplicantIdAsync(input.ApplicantId);
if (applicantAddresses != null)
{
await UpsertAddress(input, applicantAddresses, AddressType.MailingAddress, input.ApplicantId);
await UpsertAddress(input, applicantAddresses, AddressType.PhysicalAddress, input.ApplicantId);
}
}
protected virtual async Task UpsertAddress(CreateUpdateApplicantInfoDto input, List<ApplicantAddress> applicantAddresses, AddressType applicantAddressType, Guid applicantId)
{
ApplicantAddress? dbAddress = applicantAddresses.Find(address => address.AddressType == applicantAddressType);
if (dbAddress != null)
{
MapApplicantAddress(input, applicantAddressType, dbAddress);
await _applicantAddressRepository.UpdateAsync(dbAddress);
}
else
{
var newAddress = new ApplicantAddress() { AddressType = applicantAddressType, ApplicantId = applicantId };
MapApplicantAddress(input, applicantAddressType, newAddress);
await _applicantAddressRepository.InsertAsync(newAddress);
}
}
private static void MapApplicantAddress(CreateUpdateApplicantInfoDto input, AddressType applicantAddressType, ApplicantAddress address)
{
switch (applicantAddressType)
{
case AddressType.MailingAddress:
address.AddressType = AddressType.MailingAddress;
address.Street = input.MailingAddressStreet ?? "";
address.Street2 = input.MailingAddressStreet2 ?? "";
address.Unit = input.MailingAddressUnit ?? "";
address.City = input.MailingAddressCity ?? "";
address.Province = input.MailingAddressProvince ?? "";
address.Postal = input.MailingAddressPostalCode ?? "";
break;
case AddressType.PhysicalAddress:
address.AddressType = AddressType.PhysicalAddress;
address.Street = input.PhysicalAddressStreet ?? "";
address.Street2 = input.PhysicalAddressStreet2 ?? "";
address.Unit = input.PhysicalAddressUnit ?? "";
address.City = input.PhysicalAddressCity ?? "";
address.Province = input.PhysicalAddressProvince ?? "";
address.Postal = input.PhysicalAddressPostalCode ?? "";
break;
}
}
public async Task<List<GrantApplicationAssigneeDto>> GetAssigneesAsync(Guid applicationId)
{
var query = from userAssignment in await _applicationAssignmentRepository.GetQueryableAsync()
join user in await _personRepository.GetQueryableAsync() on userAssignment.AssigneeId equals user.Id
where userAssignment.ApplicationId == applicationId
select new GrantApplicationAssigneeDto
{
Id = userAssignment.Id,
AssigneeId = userAssignment.AssigneeId,
FullName = user.FullName,
Duty = userAssignment.Duty,
ApplicationId = applicationId
};
return await query.ToListAsync();
}
public async Task<GrantApplicationAssigneeDto> GetOwnerAsync(Guid ownerId)
{
var owner = await _personRepository.FindAsync(ownerId);
if (owner != null)
{
return new GrantApplicationAssigneeDto
{
Id = owner.Id,
FullName = owner.FullName
};
}
else
return new GrantApplicationAssigneeDto();
}
public async Task<ApplicationFormSubmission> GetFormSubmissionByApplicationId(Guid applicationId)
{
ApplicationFormSubmission applicationFormSubmission = new();
var application = await _applicationRepository.GetAsync(applicationId, false);
if (application != null)
{
IQueryable<ApplicationFormSubmission> queryableFormSubmissions = await _applicationFormSubmissionRepository.GetQueryableAsync();
if (queryableFormSubmissions != null)
{
var dbResult = await queryableFormSubmissions
.FirstOrDefaultAsync(a => a.ApplicationId.Equals(applicationId));
if (dbResult != null)
{
applicationFormSubmission = dbResult;
}
}
}
return applicationFormSubmission;
}
public async Task UpdateApplicationStatus(Guid[] applicationIds, Guid statusId)
{
foreach (Guid applicationId in applicationIds)
{
try
{
var application = await _applicationRepository.GetAsync(applicationId, false);
if (application != null)
{
application.ApplicationStatusId = statusId;
await _applicationRepository.UpdateAsync(application);
}
}
catch (Exception ex)
{
Debug.WriteLine(ex.ToString());
}
}
}
public async Task InsertAssigneeAsync(Guid applicationId, Guid assigneeId, string? duty)
{
try
{
var assignees = await GetAssigneesAsync(applicationId);
if (assignees == null || assignees.FindIndex(a => a.AssigneeId == assigneeId) == -1)
{
await _applicationManager.AssignUserAsync(applicationId, assigneeId, duty);
}
else
{
await _applicationManager.UpdateAssigneeAsync(applicationId, assigneeId, duty);
}
}
catch (Exception ex)
{
Debug.WriteLine(ex.ToString());
}
}
public async Task DeleteAssigneeAsync(Guid applicationId, Guid assigneeId)
{
try
{
await _applicationManager.RemoveAssigneeAsync(applicationId, assigneeId);
}
catch (Exception ex)
{
Debug.WriteLine(ex.ToString());
}
}
public async Task<List<GrantApplicationDto>> GetApplicationListAsync(List<Guid> applicationIds)
{
var applications = await
(await _applicationRepository.WithDetailsAsync())
.OrderBy(s => s.Id)
.Where(s => applicationIds.Contains(s.Id))
.ToListAsync();
return ObjectMapper.Map<List<Application>, List<GrantApplicationDto>>(applications);
}
public async Task<IList<GrantApplicationDto>> GetApplicationDetailsListAsync(List<Guid> applicationIds)
{
var query = from application in await _applicationRepository.GetQueryableAsync()
join appStatus in await _applicationStatusRepository.GetQueryableAsync() on application.ApplicationStatusId equals appStatus.Id
join applicant in await _applicantRepository.GetQueryableAsync() on application.ApplicantId equals applicant.Id
join applicationForm in await _applicationFormRepository.GetQueryableAsync() on application.ApplicationFormId equals applicationForm.Id
where applicationIds.Contains(application.Id)
select new
{
application,
appStatus,
applicant,
applicationForm
};
var result = query
.OrderBy(s => s.application.Id)
.GroupBy(s => s.application.Id)
.AsEnumerable()
.ToList();
var appDtos = new List<GrantApplicationDto>();
foreach (var grouping in result)
{
var appDto = ObjectMapper.Map<Application, GrantApplicationDto>(grouping.First().application);
appDto.Status = grouping.First().appStatus.InternalStatus;
appDto.StatusCode = grouping.First().appStatus.StatusCode;
appDto.Applicant = ObjectMapper.Map<Applicant, GrantApplicationApplicantDto>(grouping.First().applicant);
appDto.ApplicationForm = ObjectMapper.Map<ApplicationForm, ApplicationFormDto>(grouping.First().applicationForm);
appDtos.Add(appDto);
}
return new List<GrantApplicationDto>(appDtos);
}
public async Task InsertOwnerAsync(Guid applicationId, Guid? assigneeId)
{
try
{
var application = await _applicationRepository.GetAsync(applicationId, false);
if (application != null)
{
application.OwnerId = assigneeId;
await _applicationRepository.UpdateAsync(application);
}
}
catch (Exception ex)
{
Debug.WriteLine(ex.ToString());
}
}
public async Task DeleteOwnerAsync(Guid applicationId)
{
try
{
var application = await _applicationRepository.GetAsync(applicationId, false);
if (application != null)
{
application.OwnerId = null;
await _applicationRepository.UpdateAsync(application);
}
}
catch (Exception ex)
{
Debug.WriteLine(ex.ToString());
}
}
[HttpPut]
public async Task UpdateAssigneesAsync(dynamic modifiedAssignees)
{
var dynamicObject = JsonConvert.DeserializeObject<dynamic>(modifiedAssignees);
if (dynamicObject is IEnumerable)
{
Guid previousApplicationId = Guid.Empty;
foreach (JProperty item in dynamicObject)
{
Guid currentApplicationId = Guid.Parse(item.Name);
if (currentApplicationId != previousApplicationId)
{
var assignees = new List<(Guid? assigneeId, string? fullName)>();
foreach (JToken assigneeToken in item.Value.Children())
{
string? assigneeId = assigneeToken.Value<string?>("assigneeId") ?? null;
string? fullName = assigneeToken.Value<string?>("fullName") ?? null;
assignees.Add(new(assigneeId != null ? Guid.Parse(assigneeId) : null, fullName));
}
await _applicationManager.SetAssigneesAsync(currentApplicationId, assignees);
}
previousApplicationId = currentApplicationId;
}
}
}
public async Task<CommentDto> CreateCommentAsync(Guid id, CreateCommentDto dto)
{
return ObjectMapper.Map<ApplicationComment, CommentDto>((ApplicationComment)
await _commentsManager.CreateCommentAsync(id, dto.Comment, CommentType.ApplicationComment));
}
public async Task<IReadOnlyList<CommentDto>> GetCommentsAsync(Guid id)
{
return ObjectMapper.Map<IReadOnlyList<ApplicationComment>, IReadOnlyList<CommentDto>>((IReadOnlyList<ApplicationComment>)
await _commentsManager.GetCommentsAsync(id, CommentType.ApplicationComment));
}
public async Task<CommentDto> UpdateCommentAsync(Guid id, UpdateCommentDto dto)
{
try
{
return ObjectMapper.Map<ApplicationComment, CommentDto>((ApplicationComment)
await _commentsManager.UpdateCommentAsync(id, dto.CommentId, dto.Comment, CommentType.ApplicationComment));
}
catch (EntityNotFoundException)
{
throw new InvalidCommentParametersException();
}
}
public async Task<CommentDto> GetCommentAsync(Guid id, Guid commentId)
{
var comment = await _commentsManager.GetCommentAsync(id, commentId, CommentType.ApplicationComment);
return comment == null
? throw new InvalidCommentParametersException()
: ObjectMapper.Map<ApplicationComment, CommentDto>((ApplicationComment)comment);
}
public async Task<ApplicationStatusDto> GetApplicationStatusAsync(Guid id)
{
var application = await _applicationRepository.GetAsync(id, true);
return ObjectMapper.Map<ApplicationStatus, ApplicationStatusDto>(await _applicationStatusRepository.GetAsync(application.ApplicationStatusId));
}
#region APPLICATION WORKFLOW
/// <summary>
/// Fetches the list of actions and their status context for a given application.
/// </summary>
/// <param name="applicationId">The application</param>
/// <returns>A list of application actions with their state machine permitted and authorization status.</returns>
public async Task<ListResultDto<ApplicationActionDto>> GetActions(Guid applicationId, bool includeInternal = false)
{
var actionList = await _applicationManager.GetActions(applicationId);
var application = await _applicationRepository.GetAsync(applicationId, true);
// Note: Remove internal state change actions that are side-effects of domain events
var externalActionsList = actionList.Where(a => includeInternal || !a.IsInternal).ToList();
var actionDtos = ObjectMapper.Map<
List<ApplicationActionResultItem>,
List<ApplicationActionDto>>(externalActionsList);
// NOTE: Authorization is applied on the AppService layer and is false by default
// TODO: Replace placeholder loop with authorization handler mapped to permissions
// AUTHORIZATION HANDLING
actionDtos.ForEach(async item =>
{
item.IsPermitted = item.IsPermitted && (await AuthorizationService.IsGrantedAsync(application, GetActionAuthorizationRequirement(item.ApplicationAction)));
item.IsAuthorized = true;
});
return new ListResultDto<ApplicationActionDto>(actionDtos);
}
private static OperationAuthorizationRequirement GetActionAuthorizationRequirement(GrantApplicationAction triggerAction)
{
return new OperationAuthorizationRequirement { Name = triggerAction.ToString() };
}
/// <summary>
/// Transitions the Application workflow state machine given an action.
/// </summary>
/// <param name="applicationId">The application</param>
/// <param name="triggerAction">The action to be invoked on an Application</param>
public async Task<GrantApplicationDto> TriggerAction(Guid applicationId, GrantApplicationAction triggerAction)
{
// AUTHORIZATION HANDLING
var application = await _applicationRepository.GetAsync(applicationId, true);
if (!await AuthorizationService.IsGrantedAsync(application, GetActionAuthorizationRequirement(triggerAction)))
{
throw new UnauthorizedAccessException();
}
application = await _applicationManager.TriggerAction(applicationId, triggerAction);
await _localEventBus.PublishAsync(
new ApplicationChangedEvent
{
Action = triggerAction,
ApplicationId = applicationId
}
);
return ObjectMapper.Map<Application, GrantApplicationDto>(application);
}
#endregion APPLICATION WORKFLOW
public async Task<List<GrantApplicationLiteDto>> GetAllApplicationsAsync()
{
var query = from applications in await _applicationRepository.GetQueryableAsync()
select new GrantApplicationLiteDto
{
Id = applications.Id,
ProjectName = applications.ProjectName,
ReferenceNo = applications.ReferenceNo
};
return await query.ToListAsync();
}
}