560 lines
28 KiB
C#
560 lines
28 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading.Tasks;
|
|
using AyaNova.Models;
|
|
using AyaNova.Util;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Logging;
|
|
using Newtonsoft.Json.Linq;
|
|
|
|
namespace AyaNova.Biz
|
|
{
|
|
/// <summary>
|
|
/// Notification processor
|
|
/// turn notifyEvent records into inappnotification records for in app viewing and / or deliver smtp notifications seperately
|
|
///
|
|
/// </summary>
|
|
internal static class CoreJobCustomerNotify
|
|
{
|
|
private static bool NotifyIsRunning = false;
|
|
private static ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger(
|
|
"CoreJobCustomerNotify"
|
|
);
|
|
private static DateTime lastRun = DateTime.MinValue;
|
|
|
|
#if (DEBUG)
|
|
private static TimeSpan RUN_EVERY_INTERVAL = new TimeSpan(0, 0, 21); //no more frequently than once every 20 seconds
|
|
#else
|
|
private static TimeSpan RUN_EVERY_INTERVAL = new TimeSpan(0, 1, 1); //no more frequently than once every 1 minute
|
|
#endif
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// DoSweep
|
|
//
|
|
public static async Task DoWorkAsync()
|
|
{
|
|
log.LogTrace("Checking if CustomerNotify should run");
|
|
if (NotifyIsRunning)
|
|
{
|
|
log.LogTrace("CustomerNotify is running already exiting this cycle");
|
|
return;
|
|
}
|
|
//This will get triggered roughly every minute, but we don't want to deliver that frequently
|
|
if (DateTime.UtcNow - lastRun < RUN_EVERY_INTERVAL)
|
|
{
|
|
log.LogTrace(
|
|
$"CustomerNotify ran less than {RUN_EVERY_INTERVAL} ago, exiting this cycle"
|
|
);
|
|
return;
|
|
}
|
|
try
|
|
{
|
|
NotifyIsRunning = true;
|
|
log.LogDebug("CustomerNotify set to RUNNING state and starting now");
|
|
|
|
using (AyContext ct = AyaNova.Util.ServiceProviderProvider.DBContext)
|
|
{
|
|
var customerevents = await ct.CustomerNotifyEvent.AsNoTracking().ToListAsync();
|
|
log.LogDebug(
|
|
$"Found {customerevents.Count} CustomerNotifyEvents to examine for potential delivery"
|
|
);
|
|
|
|
//iterate and deliver
|
|
foreach (var customernotifyevent in customerevents)
|
|
{
|
|
//no notifications for inactive users, just delete it as if it was delivered
|
|
var CustInfo = await ct
|
|
.Customer.AsNoTracking()
|
|
.Where(x => x.Id == customernotifyevent.CustomerId)
|
|
.Select(x => new
|
|
{
|
|
x.Name,
|
|
x.Active,
|
|
x.Tags,
|
|
x.EmailAddress,
|
|
})
|
|
.FirstOrDefaultAsync();
|
|
|
|
if (!CustInfo.Active)
|
|
{
|
|
log.LogDebug(
|
|
$"Inactive Customer {CustInfo.Name}, removing notify rather than delivering it: {customernotifyevent}"
|
|
);
|
|
ct.CustomerNotifyEvent.Remove(customernotifyevent);
|
|
await ct.SaveChangesAsync();
|
|
continue;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(CustInfo.EmailAddress))
|
|
{
|
|
log.LogDebug(
|
|
$"Customer {CustInfo.Name} has no email address, removing notify rather than delivering it: {customernotifyevent}"
|
|
);
|
|
ct.CustomerNotifyEvent.Remove(customernotifyevent);
|
|
await ct.SaveChangesAsync();
|
|
continue;
|
|
}
|
|
|
|
//Get subscription for delivery
|
|
var Subscription = await ct
|
|
.CustomerNotifySubscription.AsNoTracking()
|
|
.FirstOrDefaultAsync(x =>
|
|
x.Id == customernotifyevent.CustomerNotifySubscriptionId
|
|
);
|
|
|
|
//NOTE: There is no need to separate out future delivery and immediate delivery because
|
|
// All events have an event date, it's either immediate upon creation or it's future
|
|
// but not all events have an age value including ones with future event dates,
|
|
// and the default agevalue and advancenotice are both zero regardless so the block below works for either future or immediate deliveries
|
|
|
|
var deliverAfter =
|
|
customernotifyevent.EventDate
|
|
+ Subscription.AgeValue
|
|
- Subscription.AdvanceNotice;
|
|
if (deliverAfter < DateTime.UtcNow)
|
|
{
|
|
//Do the delivery, it's kosher
|
|
await DeliverCustomerNotificationSMTP(
|
|
customernotifyevent,
|
|
Subscription,
|
|
CustInfo.EmailAddress,
|
|
ct
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.LogError(ex, $"Error processing customer notification event");
|
|
DbUtil.HandleIfDatabaseUnavailableTypeException(ex);
|
|
}
|
|
finally
|
|
{
|
|
log.LogDebug(
|
|
"CustomerNotify is done setting to not running state and tagging lastRun timestamp"
|
|
);
|
|
lastRun = DateTime.UtcNow;
|
|
NotifyIsRunning = false;
|
|
}
|
|
}
|
|
|
|
//===
|
|
private static async Task DeliverCustomerNotificationSMTP(
|
|
CustomerNotifyEvent ne,
|
|
CustomerNotifySubscription subscription,
|
|
string deliveryAddress,
|
|
AyContext ct
|
|
)
|
|
{
|
|
var DeliveryLogItem = new CustomerNotifyDeliveryLog()
|
|
{
|
|
Processed = DateTime.UtcNow,
|
|
ObjectId = ne.ObjectId,
|
|
CustomerNotifySubscriptionId = ne.CustomerNotifySubscriptionId,
|
|
Fail = false,
|
|
};
|
|
|
|
try
|
|
{
|
|
log.LogDebug($"DeliverCustomerNotificationSMTP delivering notify event: {ne}");
|
|
if (string.IsNullOrWhiteSpace(deliveryAddress))
|
|
{
|
|
DeliveryLogItem.Fail = true;
|
|
DeliveryLogItem.Error =
|
|
$"No email address provided for smtp delivery; event: {ne}";
|
|
}
|
|
else
|
|
{
|
|
if (!ServerGlobalOpsSettingsCache.Notify.SmtpDeliveryActive)
|
|
{
|
|
await NotifyEventHelper.AddOpsProblemEvent(
|
|
$"Email notifications are set to OFF at server, unable to send Customer email notification for this event:{ne}"
|
|
);
|
|
log.LogInformation(
|
|
$"** WARNING: SMTP notification is currently set to Active=False; unable to deliver Customer email notification, [CustomerId={ne.CustomerId}, Customer Notify subscription={ne.CustomerNotifySubscriptionId}]. Change this setting or remove all Customer notifications if this is permanent **"
|
|
);
|
|
DeliveryLogItem.Fail = true;
|
|
DeliveryLogItem.Error =
|
|
$"Email notifications are set to OFF at server, unable to send Customer email notification for this event: {ne}";
|
|
}
|
|
else
|
|
{
|
|
//BUILD SUBJECT AND BODY FROM TOKENS IF REQUIRED
|
|
var Subject = subscription.Subject;
|
|
var Body = subscription.Template;
|
|
|
|
if (Subject.Contains("{{") || Body.Contains("{{"))
|
|
{
|
|
//fetch the object with viz fields for easy templatization
|
|
switch (ne.AyaType)
|
|
{
|
|
case AyaType.Quote:
|
|
{
|
|
var qt = await ct
|
|
.Quote.AsNoTracking()
|
|
.FirstOrDefaultAsync(z => z.Id == ne.ObjectId);
|
|
if (qt == null)
|
|
{
|
|
//maybe deleted, this can't proceed
|
|
throw new ApplicationException(
|
|
$"Unable to make delivery for customer notify event as Quote {ne.Name} was not found during delivery, deleted?"
|
|
);
|
|
}
|
|
|
|
var CustomerName = await ct
|
|
.Customer.AsNoTracking()
|
|
.Where(x => x.Id == qt.CustomerId)
|
|
.Select(x => x.Name)
|
|
.FirstOrDefaultAsync();
|
|
Subject = SetQuoteTokens(Subject, qt, CustomerName);
|
|
Body = SetQuoteTokens(Body, qt, CustomerName);
|
|
}
|
|
break;
|
|
case AyaType.WorkOrder:
|
|
{
|
|
var wo = await ct
|
|
.WorkOrder.AsNoTracking()
|
|
.FirstOrDefaultAsync(z => z.Id == ne.ObjectId);
|
|
if (wo == null)
|
|
{
|
|
//maybe deleted, this can't proceed
|
|
throw new ApplicationException(
|
|
$"Unable to make delivery for customer notify event as WorkOrder {ne.Name} was not found during delivery, deleted?"
|
|
);
|
|
}
|
|
|
|
var CustomerName = await ct
|
|
.Customer.AsNoTracking()
|
|
.Where(x => x.Id == wo.CustomerId)
|
|
.Select(x => x.Name)
|
|
.FirstOrDefaultAsync();
|
|
var StatusName = await ct
|
|
.WorkOrderStatus.AsNoTracking()
|
|
.Where(x => x.Id == wo.LastStatusId)
|
|
.Select(x => x.Name)
|
|
.FirstOrDefaultAsync();
|
|
Subject = SetWorkOrderTokens(
|
|
Subject,
|
|
wo,
|
|
CustomerName,
|
|
StatusName
|
|
);
|
|
Body = SetWorkOrderTokens(
|
|
Body,
|
|
wo,
|
|
CustomerName,
|
|
StatusName
|
|
);
|
|
}
|
|
break;
|
|
case AyaType.CustomerServiceRequest:
|
|
{
|
|
var csr = await ct
|
|
.CustomerServiceRequest.AsNoTracking()
|
|
.FirstOrDefaultAsync(z => z.Id == ne.ObjectId);
|
|
if (csr == null)
|
|
{
|
|
//maybe deleted, this can't proceed
|
|
throw new ApplicationException(
|
|
$"Unable to make delivery for customer notify event as CustomerServiceRequest {ne.Name} was not found during delivery, deleted?"
|
|
);
|
|
}
|
|
|
|
var CustomerName = await ct
|
|
.Customer.AsNoTracking()
|
|
.Where(x => x.Id == csr.CustomerId)
|
|
.Select(x => x.Name)
|
|
.FirstOrDefaultAsync();
|
|
var UserName = await ct
|
|
.User.AsNoTracking()
|
|
.Where(x => x.Id == csr.RequestedByUserId)
|
|
.Select(x => x.Name)
|
|
.FirstOrDefaultAsync();
|
|
Subject = SetCSRTokens(
|
|
Subject,
|
|
csr,
|
|
CustomerName,
|
|
UserName
|
|
);
|
|
Body = SetCSRTokens(Body, csr, CustomerName, UserName);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
IMailer m = AyaNova.Util.ServiceProviderProvider.Mailer;
|
|
//generate report if applicable
|
|
bool isReportableEvent = false;
|
|
switch (ne.EventType)
|
|
{
|
|
case NotifyEventType.QuoteStatusChange:
|
|
case NotifyEventType.WorkorderCompleted:
|
|
case NotifyEventType.WorkorderStatusChange:
|
|
isReportableEvent = true;
|
|
break;
|
|
}
|
|
if (isReportableEvent && subscription.LinkReportId != null)
|
|
{
|
|
long subTranslationId = (long)subscription.TranslationId;
|
|
|
|
ReportBiz biz = new ReportBiz(
|
|
ct,
|
|
1,
|
|
subTranslationId,
|
|
AuthorizationRoles.BizAdmin
|
|
);
|
|
//example with workorder report
|
|
//{"AType":34,"selectedRowIds":[355],"ReportId":9,"ClientMeta":{"UserName":"AyaNova SuperUser","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOiIxNjQ2NzgyNTc4IiwiaXNzIjoiYXlhbm92YS5jb20iLCJpZCI6IjEifQ.ad7Acq54JCRGitDWKDJFFnqKkidbdaKaFmj-RA_RG5E","DownloadToken":"NdoU8ca3LG4L39Tj2oi3UReeeM7FLevTgbgopTPhGbA","TimeZoneName":"America/Los_Angeles","LanguageName":"en-US","Hour12":true,"CurrencyName":"USD","DefaultLocale":"en","PDFDate":"3/3/22","PDFTime":"3:38 PM"}}
|
|
|
|
var reportRequest = new DataListReportRequest();
|
|
|
|
reportRequest.AType = ne.AyaType;
|
|
reportRequest.ReportId = (long)subscription.LinkReportId;
|
|
reportRequest.SelectedRowIds = new long[] { ne.ObjectId };
|
|
var jwt = Api.Controllers.AuthController.GenRpt(subTranslationId);
|
|
|
|
//this could be adjusted by culture if we allow user to set a culture but that's getting a bit into the weeds, likely the server default is fine
|
|
var pdfDate = new DateTime().ToShortDateString();
|
|
var pdfTime = new DateTime().ToShortTimeString();
|
|
var h12 = subscription.Hour12 ? "true" : "false";
|
|
reportRequest.ClientMeta = JToken.Parse(
|
|
$"{{'UserName':'-','Authorization':'Bearer {jwt}','TimeZoneName':'{subscription.TimeZoneOverride}','LanguageName':'{subscription.LanguageOverride}','Hour12':{h12},'CurrencyName':'{subscription.CurrencyName}','DefaultLocale':'en','PDFDate':'{pdfDate}','PDFTime':'{pdfTime}'}}"
|
|
);
|
|
//get port number
|
|
var match = System.Text.RegularExpressions.Regex.Match(
|
|
ServerBootConfig.AYANOVA_USE_URLS,
|
|
"[0-9]+"
|
|
);
|
|
var API_URL =
|
|
$"http://127.0.0.1:{match.Value}/api/{AyaNovaVersion.CurrentApiVersion}/";
|
|
var jobid = await biz.RequestRenderReport(
|
|
reportRequest,
|
|
DateTime.UtcNow.AddMinutes(
|
|
ServerBootConfig.AYANOVA_REPORT_RENDERING_TIMEOUT
|
|
),
|
|
API_URL,
|
|
ServerBootConfig.CUSTOMER_NOTIFICATION_ATTACHED_REPORT_RENDER_USERNAME
|
|
);
|
|
if (jobid == null)
|
|
{
|
|
throw new ApplicationException(
|
|
$"Report render job id is null failed to start"
|
|
);
|
|
}
|
|
else
|
|
{
|
|
bool done = false;
|
|
DateTime bailAfter = DateTime.Now.AddMinutes(
|
|
ServerBootConfig.AYANOVA_REPORT_RENDERING_TIMEOUT
|
|
);
|
|
while (!done && DateTime.Now < bailAfter)
|
|
{
|
|
var status = await JobsBiz.GetJobStatusAsync((Guid)jobid);
|
|
switch (status)
|
|
{
|
|
case JobStatus.Completed:
|
|
{
|
|
done = true;
|
|
//get job logs and parse file name from it
|
|
JobOperationsBiz jobopsbiz = new JobOperationsBiz(
|
|
ct,
|
|
1,
|
|
AuthorizationRoles.BizAdmin
|
|
);
|
|
List<JobOperationsLogInfoItem> log =
|
|
await jobopsbiz.GetJobLogListAsync((Guid)jobid);
|
|
var lastLog = log[log.Count - 1];
|
|
var lastLogJ = JObject.Parse(lastLog.StatusText);
|
|
var path = (string)lastLogJ["reportfilename"];
|
|
var FilePath = FileUtil.GetFullPathForTemporaryFile(
|
|
path
|
|
);
|
|
var FileName = FileUtil
|
|
.StringToSafeFileName(
|
|
await TranslationBiz.GetTranslationStaticAsync(
|
|
ne.AyaType.ToString(),
|
|
subTranslationId,
|
|
ct
|
|
) + $"-{ne.Name}.pdf"
|
|
)
|
|
.ToLowerInvariant();
|
|
await m.SendEmailAsync(
|
|
deliveryAddress,
|
|
Subject,
|
|
Body,
|
|
ServerGlobalOpsSettingsCache.Notify,
|
|
FilePath,
|
|
FileName
|
|
);
|
|
break;
|
|
}
|
|
case JobStatus.Failed:
|
|
case JobStatus.Absent:
|
|
throw new ApplicationException(
|
|
$"REPORT RENDER JOB {jobid} started but failed"
|
|
);
|
|
}
|
|
}
|
|
if (!done)
|
|
throw new TimeoutException(
|
|
"JOB FAILED DUE TO REPORT RENDER TIMEOUT"
|
|
);
|
|
}
|
|
}
|
|
else
|
|
await m.SendEmailAsync(
|
|
deliveryAddress,
|
|
Subject,
|
|
Body,
|
|
ServerGlobalOpsSettingsCache.Notify
|
|
);
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await NotifyEventHelper.AddOpsProblemEvent("SMTP Customer Notification failed", ex);
|
|
DeliveryLogItem.Fail = true;
|
|
DeliveryLogItem.Error =
|
|
$"SMTP Notification failed to deliver for this Customer notify event: {ne}, message: {ex.Message}";
|
|
log.LogDebug(ex, $"DeliverSMTP Failure delivering Customer notify event: {ne}");
|
|
}
|
|
finally
|
|
{
|
|
//remove event no matter what
|
|
ct.CustomerNotifyEvent.Remove(ne);
|
|
|
|
//add delivery log item
|
|
await ct.CustomerNotifyDeliveryLog.AddAsync(DeliveryLogItem);
|
|
await ct.SaveChangesAsync();
|
|
}
|
|
}
|
|
|
|
private static string SetQuoteTokens(string TheField, Quote qt, string CustomerName)
|
|
{
|
|
MatchCollection matches = Regex.Matches(
|
|
TheField,
|
|
@"\{{(.|\n)*?\}}",
|
|
RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled
|
|
);
|
|
//{{.*?}}
|
|
foreach (Match KeyMatch in matches)
|
|
{
|
|
switch (KeyMatch.Value)
|
|
{
|
|
case "{{Customer}}":
|
|
TheField = TheField.Replace(KeyMatch.Value, CustomerName);
|
|
break;
|
|
case "{{QuoteIntroduction}}":
|
|
TheField = TheField.Replace(KeyMatch.Value, qt.Introduction);
|
|
break;
|
|
case "{{WorkOrderCustomerContactName}}":
|
|
TheField = TheField.Replace(KeyMatch.Value, qt.CustomerContactName);
|
|
break;
|
|
case "{{WorkOrderCustomerReferenceNumber}}":
|
|
TheField = TheField.Replace(KeyMatch.Value, qt.CustomerReferenceNumber);
|
|
break;
|
|
case "{{WorkOrderSummary}}":
|
|
TheField = TheField.Replace(KeyMatch.Value, qt.Notes);
|
|
break;
|
|
case "{{QuoteSerialNumber}}":
|
|
TheField = TheField.Replace(KeyMatch.Value, qt.Serial.ToString());
|
|
break;
|
|
}
|
|
}
|
|
|
|
return TheField;
|
|
}
|
|
|
|
private static string SetWorkOrderTokens(
|
|
string TheField,
|
|
WorkOrder wo,
|
|
string CustomerName,
|
|
string statusName
|
|
)
|
|
{
|
|
MatchCollection matches = Regex.Matches(
|
|
TheField,
|
|
@"\{{(.|\n)*?\}}",
|
|
RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled
|
|
);
|
|
//{{.*?}}
|
|
foreach (Match KeyMatch in matches)
|
|
{
|
|
switch (KeyMatch.Value)
|
|
{
|
|
case "{{Customer}}":
|
|
TheField = TheField.Replace(KeyMatch.Value, CustomerName);
|
|
break;
|
|
case "{{WorkOrderStatus}}":
|
|
TheField = TheField.Replace(KeyMatch.Value, statusName);
|
|
break;
|
|
case "{{WorkOrderCustomerContactName}}":
|
|
TheField = TheField.Replace(KeyMatch.Value, wo.CustomerContactName);
|
|
break;
|
|
case "{{WorkOrderCustomerReferenceNumber}}":
|
|
TheField = TheField.Replace(KeyMatch.Value, wo.CustomerReferenceNumber);
|
|
break;
|
|
case "{{WorkOrderSummary}}":
|
|
TheField = TheField.Replace(KeyMatch.Value, wo.Notes);
|
|
break;
|
|
case "{{WorkOrderSerialNumber}}":
|
|
TheField = TheField.Replace(KeyMatch.Value, wo.Serial.ToString());
|
|
break;
|
|
}
|
|
}
|
|
|
|
return TheField;
|
|
}
|
|
|
|
private static string SetCSRTokens(
|
|
string TheField,
|
|
CustomerServiceRequest csr,
|
|
string CustomerName,
|
|
string requestedBy
|
|
)
|
|
{
|
|
MatchCollection matches = Regex.Matches(
|
|
TheField,
|
|
@"\{{(.|\n)*?\}}",
|
|
RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled
|
|
);
|
|
//{{.*?}}
|
|
foreach (Match KeyMatch in matches)
|
|
{
|
|
switch (KeyMatch.Value)
|
|
{
|
|
case "{{Customer}}":
|
|
TheField = TheField.Replace(KeyMatch.Value, CustomerName);
|
|
break;
|
|
case "{{CustomerServiceRequestRequestedBy}}":
|
|
TheField = TheField.Replace(KeyMatch.Value, requestedBy);
|
|
break;
|
|
case "{{CustomerServiceRequestCustomerReferenceNumber}}":
|
|
TheField = TheField.Replace(KeyMatch.Value, csr.CustomerReferenceNumber);
|
|
break;
|
|
case "{{CustomerServiceRequestTitle}}":
|
|
TheField = TheField.Replace(KeyMatch.Value, csr.Name);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return TheField;
|
|
}
|
|
//===
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/////////////////////////////////////////////////////////////////////
|
|
} //eoc
|
|
} //eons
|