Skip to content

Commit 24224ba

Browse files
authored
Merge pull request #775 from bcgov/yj
Yj
2 parents bc45053 + 4d39ad3 commit 24224ba

File tree

7 files changed

+227
-25
lines changed

7 files changed

+227
-25
lines changed

server/StrDss.Api/Controllers/DelistingController.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,27 @@ public async Task<ActionResult> SendBatchtakedownNotice([FromForm] BatchTakedown
211211
return NoContent();
212212
}
213213

214+
[ApiAuthorize(Permissions.ProvinceAction)]
215+
[HttpPost("complianceorders/preview", Name = "GetComplianceOrdersFromListingPreview")]
216+
public async Task<ActionResult> GetComplianceOrdersFromListingPreview(ComplianceOrderDto[] requests)
217+
{
218+
await Task.CompletedTask;
219+
220+
var preview = new EmailPreview
221+
{
222+
Content = "To: john.doe@my.email, jane.smith@my.email<br/><br/><br/>Bcc: young-jin.chung@gov.bc.ca<br/><br/>\r\nDear Host,<br/><br/>\r\nThis message has been sent to you by B.C.'s Short-Term Rental Compliance Unit regarding your short-term rental<br/>\r\nlisting: <b>https://example.com/1000012/</b><br/><br/>\r\ntesting<br/>\r\n"
223+
};
224+
225+
return Ok(preview);
226+
}
227+
228+
[ApiAuthorize(Permissions.ProvinceAction)]
229+
[HttpPost("complianceorders", Name = "CreateComplianceOrdersFromListingPreview")]
230+
public async Task<ActionResult> CreateComplianceOrdersFromListingPreview(ComplianceOrderDto[] requests)
231+
{
232+
await Task.CompletedTask;
214233

234+
return NoContent();
235+
}
215236
}
216237
}

server/StrDss.Api/Program.cs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -221,17 +221,17 @@
221221

222222
app.Use(async (context, next) =>
223223
{
224-
context.Response.Headers.Add("content-security-policy", $"default-src 'self'; style-src 'self' 'img-src 'self' data:; frame-ancestors 'self'; object-src 'none'; base-uri 'self'; form-action 'self';");
225-
context.Response.Headers.Add("strict-transport-security", "max-age=15768000; includeSubDomains; preload");
226-
context.Response.Headers.Add("x-content-type-options", "nosniff");
227-
context.Response.Headers.Add("x-frame-options", "SAMEORIGIN");
228-
context.Response.Headers.Add("x-xss-protection", "0");
229-
context.Response.Headers.Add("permissions-policy", "geolocation=(),midi=(),sync-xhr=(),microphone=(),camera=(),magnetometer=(),gyroscope=(),fullscreen=(self),payment=()");
230-
context.Response.Headers.Add("referrer-policy", "strict-origin");
231-
context.Response.Headers.Add("x-dns-prefetch-control", "off");
232-
context.Response.Headers.Add("cache-control", "no-cache, no-store, must-revalidate");
233-
context.Response.Headers.Add("pragma", "no-cache");
234-
context.Response.Headers.Add("expires", "0");
224+
context.Response.Headers.Append("content-security-policy", $"default-src 'self'; style-src 'self'; img-src 'self' data:; frame-ancestors 'self'; object-src 'none'; base-uri 'self'; form-action 'self';");
225+
context.Response.Headers.Append("strict-transport-security", "max-age=15768000; includeSubDomains; preload");
226+
context.Response.Headers.Append("x-content-type-options", "nosniff");
227+
context.Response.Headers.Append("x-frame-options", "SAMEORIGIN");
228+
context.Response.Headers.Append("x-xss-protection", "0");
229+
context.Response.Headers.Append("permissions-policy", "geolocation=(), midi=(), sync-xhr=(), microphone=(), camera=(), magnetometer=(), gyroscope=(), fullscreen=(self), payment=()");
230+
context.Response.Headers.Append("referrer-policy", "strict-origin");
231+
context.Response.Headers.Append("x-dns-prefetch-control", "off");
232+
context.Response.Headers.Append("cache-control", "no-cache, no-store, must-revalidate");
233+
context.Response.Headers.Append("pragma", "no-cache");
234+
context.Response.Headers.Append("expires", "0");
235235
await next.Invoke();
236236
});
237237

server/StrDss.Api/StrDss.Api.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
<Nullable>enable</Nullable>
66
<ImplicitUsings>enable</ImplicitUsings>
77
<GenerateDocumentationFile>True</GenerateDocumentationFile>
8+
<NoWarn>1591</NoWarn>
89
</PropertyGroup>
910

1011
<ItemGroup>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace StrDss.Model.DelistingDtos
4+
{
5+
public class ComplianceOrderDto
6+
{
7+
public long RentalListingId { get; set; }
8+
public List<string> BccList { get; set; } = new List<string>();
9+
public string Comment { get; set; } = "";
10+
[JsonIgnore]
11+
public List<string> HostEmails { get; set; } = new List<string>();
12+
}
13+
}

server/StrDss.Service/DelistingService.cs

Lines changed: 145 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using StrDss.Model;
1010
using StrDss.Model.DelistingDtos;
1111
using StrDss.Model.OrganizationDtos;
12+
using StrDss.Model.RentalReportDtos;
1213
using StrDss.Service.CsvHelpers;
1314
using StrDss.Service.EmailTemplates;
1415
using System.Text.RegularExpressions;
@@ -28,6 +29,8 @@ public interface IDelistingService
2829
Task<(Dictionary<string, List<string>> errors, EmailPreview preview)> GetTakedownNoticesFromListingPreviewAsync(TakedownNoticesFromListingDto[] listings);
2930
Task<Dictionary<string, List<string>>> CreateTakedownRequestsFromListingAsync(TakedownRequestsFromListingDto[] listings);
3031
Task<(Dictionary<string, List<string>> errors, EmailPreview preview)> GetTakedownRequestsFromListingPreviewAsync(TakedownRequestsFromListingDto[] listings);
32+
Task<(Dictionary<string, List<string>> errors, EmailPreview preview)> GetComplianceOrdersFromListingPreviewAsync(ComplianceOrderDto[] listings);
33+
Task<Dictionary<string, List<string>>> CreateComplianceOrdersFromListingAsync(ComplianceOrderDto[] listings);
3134
}
3235
public class DelistingService : ServiceBase, IDelistingService
3336
{
@@ -268,7 +271,7 @@ public async Task<Dictionary<string, List<string>>> CreateTakedownNoticesFromLis
268271
var template = CreateTakedownNoticeTemplate(listing, rentalListing);
269272
templates.Add(template);
270273

271-
ValidateCcListEmails(listing.CcList, emailRegex, errors);
274+
ValidateEmails(listing.CcList, emailRegex, "ccList", errors);
272275
ValidateLocalGovernmentContactEmail(listing, emailRegex, errors);
273276
ValidateAndSetHostEmails(listing, rentalListing, emailRegex, template, errors);
274277

@@ -410,7 +413,7 @@ private async Task SendTakedownNoticeEmailFromListingAsync(TakedownNoticesFromLi
410413
var template = CreateTakedownNoticeTemplate(listing, rentalListing);
411414
templates.Add(template);
412415

413-
ValidateCcListEmails(listing.CcList, emailRegex, errors);
416+
ValidateEmails(listing.CcList, emailRegex, "ccList", errors);
414417
ValidateLocalGovernmentContactEmail(listing, emailRegex, errors);
415418
ValidateAndSetHostEmails(listing, rentalListing, emailRegex, template, errors);
416419

@@ -478,7 +481,7 @@ private async Task ProcessListings(TakedownRequestsFromListingDto[] listings, Di
478481

479482
foreach (var listing in listings)
480483
{
481-
ValidateCcListEmails(listing.CcList, emailRegex, errors);
484+
ValidateEmails(listing.CcList, emailRegex, "ccList", errors);
482485

483486
var rentalListing = await _listingRepo.GetRentalListingForTakedownAction(listing.RentalListingId, false);
484487

@@ -495,13 +498,13 @@ private async Task ProcessListings(TakedownRequestsFromListingDto[] listings, Di
495498
}
496499
}
497500

498-
private void ValidateCcListEmails(List<string> ccList, RegexInfo emailRegex, Dictionary<string, List<string>> errors)
501+
private void ValidateEmails(List<string> emails, RegexInfo emailRegex, string field, Dictionary<string, List<string>> errors)
499502
{
500-
foreach (var email in ccList)
503+
foreach (var email in emails)
501504
{
502505
if (!Regex.IsMatch(email, emailRegex.Regex))
503506
{
504-
errors.AddItem("ccList", $"Email ({email}) is invalid");
507+
errors.AddItem(field, $"Email ({email}) is invalid");
505508
}
506509
}
507510
}
@@ -630,7 +633,7 @@ public async Task<Dictionary<string, List<string>>> CreateTakedownRequestAsync(T
630633
return errors;
631634
}
632635

633-
await SendTakedownRequestAsync(dto, platform, lg);
636+
await SendTakedownRequestAsync(dto, platform!, lg!);
634637

635638
return errors;
636639
}
@@ -780,11 +783,11 @@ public async Task ProcessTakedownRequestBatchEmailsAsync()
780783

781784
try
782785
{
783-
await ProcessTakedownRequestBatchEmailAsync(platform, allEmails);
786+
await ProcessTakedownRequestBatchEmailAsync(platform!, allEmails);
784787
}
785788
catch (Exception ex)
786789
{
787-
_logger.LogError($"Error while processing '{EmailMessageTypes.BatchTakedownRequest}' email for {platform.OrganizationNm}");
790+
_logger.LogError($"Error while processing '{EmailMessageTypes.BatchTakedownRequest}' email for {platform!.OrganizationNm}");
788791
_logger.LogError(ex.ToString());
789792
//send email to admin?
790793
}
@@ -883,7 +886,7 @@ public async Task<Dictionary<string, List<string>>> SendBatchTakedownRequestAsyn
883886
}
884887

885888
//the existence of the contact email has been validated above
886-
var contacts = platform.ContactPeople
889+
var contacts = platform!.ContactPeople
887890
.Where(x => x.IsPrimary && x.EmailAddressDsc.IsNotEmpty() && x.EmailMessageType == EmailMessageTypes.BatchTakedownRequest)
888891
.Select(x => x.EmailAddressDsc)
889892
.ToArray();
@@ -1067,5 +1070,137 @@ private async Task<Dictionary<string, List<string>>> ValidateBatchTakedownNotice
10671070

10681071
return errors;
10691072
}
1073+
public async Task<(Dictionary<string, List<string>> errors, EmailPreview preview)> GetComplianceOrdersFromListingPreviewAsync(ComplianceOrderDto[] listings)
1074+
{
1075+
var errors = new Dictionary<string, List<string>>();
1076+
var templates = new List<ComplianceOrderFromListing>();
1077+
1078+
await ProcessComplianceOrderListings(listings, errors, templates);
1079+
1080+
if (errors.Count > 0)
1081+
{
1082+
return (errors, new EmailPreview());
1083+
}
1084+
1085+
var template = templates.FirstOrDefault();
1086+
1087+
if (template == null)
1088+
{
1089+
errors.AddItem("template", "Wasn't able to create email templates from the selected listings");
1090+
return (errors, new EmailPreview());
1091+
}
1092+
1093+
template.Preview = true;
1094+
1095+
return (errors, new EmailPreview()
1096+
{
1097+
Content = template.GetHtmlPreview()
1098+
});
1099+
}
1100+
1101+
private async Task ProcessComplianceOrderListings(ComplianceOrderDto[] listings, Dictionary<string, List<string>> errors,
1102+
List<ComplianceOrderFromListing> templates)
1103+
{
1104+
var emailRegex = RegexDefs.GetRegexInfo(RegexDefs.Email);
1105+
1106+
foreach (var listing in listings)
1107+
{
1108+
var rentalListing = await _listingRepo.GetRentalListing(listing.RentalListingId, false);
1109+
1110+
if (rentalListing == null) continue;
1111+
1112+
var template = CreateComplianceOrderTemplate(listing, rentalListing);
1113+
1114+
ValidateEmails(listing.BccList, emailRegex, "bccList", errors);
1115+
1116+
listing.HostEmails = GetValidHostEmails(rentalListing.Hosts.ToArray(), emailRegex);
1117+
1118+
template.OrgCd = rentalListing.OfferingOrganizationCd!;
1119+
template.RentalListingId = rentalListing.RentalListingId ?? 0;
1120+
template.To = listing.HostEmails;
1121+
template.Bcc = listing.BccList;
1122+
template.Comment = listing.Comment;
1123+
templates.Add(template);
1124+
}
1125+
}
1126+
public async Task<Dictionary<string, List<string>>> CreateComplianceOrdersFromListingAsync(ComplianceOrderDto[] listings)
1127+
{
1128+
var errors = new Dictionary<string, List<string>>();
1129+
var emailRegex = RegexDefs.GetRegexInfo(RegexDefs.Email);
1130+
var templates = new List<ComplianceOrderFromListing>();
1131+
1132+
await ProcessComplianceOrderListings(listings, errors, templates);
1133+
1134+
if (errors.Count > 0)
1135+
{
1136+
return errors;
1137+
}
1138+
1139+
await SendComplianceOrderEmailsFromListingAsync(listings, templates, errors);
1140+
1141+
return errors;
1142+
}
1143+
1144+
private ComplianceOrderFromListing CreateComplianceOrderTemplate(ComplianceOrderDto listing, RentalListingViewDto rentalListing)
1145+
{
1146+
return new ComplianceOrderFromListing(_emailService)
1147+
{
1148+
Url = rentalListing.PlatformListingUrl ?? "",
1149+
ListingId = rentalListing.PlatformListingNo,
1150+
Comment = listing.Comment,
1151+
Info = $"{rentalListing.OfferingOrganizationCd}-{rentalListing.PlatformListingNo}"
1152+
};
1153+
}
1154+
1155+
private List<string> GetValidHostEmails(RentalListingContactDto[] contacts, RegexInfo emailRegex)
1156+
{
1157+
return contacts
1158+
.Where(contact => !string.IsNullOrEmpty(contact.EmailAddressDsc) && Regex.IsMatch(contact.EmailAddressDsc, emailRegex.Regex))
1159+
.Select(contact => contact.EmailAddressDsc ?? "")
1160+
.ToList();
1161+
}
1162+
1163+
private async Task SendComplianceOrderEmailsFromListingAsync(ComplianceOrderDto[] listings, List<ComplianceOrderFromListing> templates, Dictionary<string, List<string>> errors)
1164+
{
1165+
foreach (var template in templates)
1166+
{
1167+
try
1168+
{
1169+
await SendComplianceOrderEmailFromListingAsync(listings, template);
1170+
}
1171+
catch (Exception ex)
1172+
{
1173+
_logger.LogError(ex.ToString());
1174+
errors.AddItem($"{template.OrgCd}-{template.ListingId}", "Failed to send email for the listing.");
1175+
}
1176+
}
1177+
}
1178+
1179+
private async Task SendComplianceOrderEmailFromListingAsync(ComplianceOrderDto[] listings, ComplianceOrderFromListing template)
1180+
{
1181+
var listing = listings.First(x => x.RentalListingId == template.RentalListingId);
1182+
1183+
var emailEntity = new DssEmailMessage
1184+
{
1185+
EmailMessageType = template.EmailMessageType,
1186+
MessageDeliveryDtm = DateTime.UtcNow,
1187+
MessageTemplateDsc = template.GetContent(),
1188+
IsSubmitterCcRequired = true, //todo:
1189+
UnreportedListingNo = template.ListingId,
1190+
HostEmailAddressDsc = listing.HostEmails.FirstOrDefault(),
1191+
LgEmailAddressDsc = null,
1192+
CcEmailAddressDsc = string.Join("; ", template.Bcc),
1193+
UnreportedListingUrl = template.Url,
1194+
InitiatingUserIdentityId = _currentUser.Id,
1195+
AffectedByUserIdentityId = null,
1196+
ConcernedWithRentalListingId = listing.RentalListingId,
1197+
};
1198+
1199+
await _emailRepo.AddEmailMessage(emailEntity);
1200+
1201+
emailEntity.ExternalMessageNo = await template.SendEmail();
1202+
1203+
_unitOfWork.Commit();
1204+
}
10701205
}
10711206
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using StrDss.Common;
2+
3+
namespace StrDss.Service.EmailTemplates
4+
{
5+
public class ComplianceOrderFromListing : EmailTemplateBase
6+
{
7+
public ComplianceOrderFromListing(IEmailMessageService emailService)
8+
: base(emailService)
9+
{
10+
EmailMessageType = EmailMessageTypes.ComplianceOrder;
11+
From = Environment.GetEnvironmentVariable("STR_CEU_EMAIL") ?? From;
12+
Subject = "New mail from the Short-term Rental Compliance and Enforcement Unit";
13+
}
14+
15+
public long RentalListingId { get; set; }
16+
public string OrgCd { get; set; } = "";
17+
public string Url { get; set; } = "";
18+
public string? ListingId { get; set; }
19+
public string Comment { get; set; } = "";
20+
21+
public override string GetContent()
22+
{
23+
return (Preview ? GetPreviewHeader() : "") + $@"
24+
Dear Host,<br/><br/>
25+
This message has been sent to you by B.C.'s Short-Term Rental Compliance Unit regarding your short-term rental<br/>
26+
listing: <b>{Url}</b><br/><br/>
27+
{Comment}<br/>
28+
";
29+
}
30+
31+
public string GetHtmlPreview()
32+
{
33+
return GetContent();
34+
}
35+
}
36+
}

server/StrDss.Test/DelistingServiceShould.cs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ public async Task ValidateDelistingWarning_ValidDto_ReturnsNoErrors(
1919
OrganizationDto platform,
2020
[Frozen] Mock<IConfiguration> configMock,
2121
[Frozen] Mock<IOrganizationService> orgServiceMock,
22-
[Frozen] Mock<IEmailMessageService> emailServiceMock,
2322
[Frozen] Mock<ICurrentUser> currentUserMock,
2423
DelistingService sut)
2524
{
@@ -352,7 +351,6 @@ public async Task SendDelistingWarningAsync_WhenCalled_ShouldSendEmail(
352351
public async Task SendDelistingWarningAsync_WhenHostEmailIsNotEmpty_AddsHostEmailToToList(
353352
TakedownNoticeCreateDto dto,
354353
OrganizationDto platform,
355-
[Frozen] Mock<IEmailMessageService> emailServiceMock,
356354
[Frozen] Mock<ICurrentUser> currentUserMock,
357355
[Frozen] Mock<IOrganizationService> orgServiceMock,
358356
DelistingService sut)
@@ -383,7 +381,6 @@ public async Task SendDelistingWarningAsync_WhenHostEmailIsNotEmpty_AddsHostEmai
383381
[AutoDomainData]
384382
public async Task SendDelistingWarningAsync_WhenHostEmailIsEmpty_DoesNotAddHostEmailToToList(
385383
TakedownNoticeCreateDto dto,
386-
[Frozen] Mock<IEmailMessageService> emailServiceMock,
387384
DelistingService sut)
388385
{
389386
// Arrange
@@ -401,7 +398,6 @@ public async Task SendDelistingWarningAsync_WhenHostEmailIsEmpty_DoesNotAddHostE
401398
public async Task SendDelistingWarningAsync_Always_AddsCurrentUserEmailToCcList(
402399
TakedownNoticeCreateDto dto,
403400
OrganizationDto platform,
404-
[Frozen] Mock<IEmailMessageService> emailServiceMock,
405401
[Frozen] Mock<ICurrentUser> currentUserMock,
406402
[Frozen] Mock<IOrganizationService> orgServiceMock,
407403
DelistingService sut)

0 commit comments

Comments
 (0)