case 4632

This commit is contained in:
2025-02-18 21:08:56 +00:00
parent 406e42df4e
commit 9d7c575dd1
5 changed files with 813 additions and 345 deletions

View File

@@ -1,32 +1,33 @@
using System;
using System.Linq;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using AyaNova.Models;
using AyaNova.Util;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
using System.Text.RegularExpressions;
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 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
#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
////////////////////////////////////////////////////////////////////////////////////////////////
@@ -43,7 +44,9 @@ namespace AyaNova.Biz
//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");
log.LogTrace(
$"CustomerNotify ran less than {RUN_EVERY_INTERVAL} ago, exiting this cycle"
);
return;
}
try
@@ -54,18 +57,31 @@ namespace AyaNova.Biz
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");
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();
//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}");
log.LogDebug(
$"Inactive Customer {CustInfo.Name}, removing notify rather than delivering it: {customernotifyevent}"
);
ct.CustomerNotifyEvent.Remove(customernotifyevent);
await ct.SaveChangesAsync();
continue;
@@ -73,29 +89,42 @@ namespace AyaNova.Biz
if (string.IsNullOrWhiteSpace(CustInfo.EmailAddress))
{
log.LogDebug($"Customer {CustInfo.Name} has no email address, removing notify rather than delivering it: {customernotifyevent}");
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);
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
// 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;
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);
await DeliverCustomerNotificationSMTP(
customernotifyevent,
Subscription,
CustInfo.EmailAddress,
ct
);
}
}
}
}
catch (Exception ex)
{
@@ -104,25 +133,28 @@ namespace AyaNova.Biz
}
finally
{
log.LogDebug("CustomerNotify is done setting to not running state and tagging lastRun timestamp");
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)
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
Fail = false,
};
try
@@ -131,21 +163,25 @@ namespace AyaNova.Biz
if (string.IsNullOrWhiteSpace(deliveryAddress))
{
DeliveryLogItem.Fail = true;
DeliveryLogItem.Error = $"No email address provided for smtp delivery; event: {ne}";
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 **");
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}";
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;
@@ -157,54 +193,98 @@ namespace AyaNova.Biz
{
case AyaType.Quote:
{
var qt = await ct.Quote.AsNoTracking().FirstOrDefaultAsync(z => z.Id == ne.ObjectId);
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?");
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();
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);
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?");
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);
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);
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?");
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);
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;
@@ -220,7 +300,12 @@ namespace AyaNova.Biz
{
long subTranslationId = (long)subscription.TranslationId;
ReportBiz biz = new ReportBiz(ct, 1, subTranslationId, AuthorizationRoles.BizAdmin);
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"}}
@@ -235,61 +320,106 @@ namespace AyaNova.Biz
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}'}}");
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, "CUSTOMER NOTIFICATION - NO USER");
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");
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);
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;
}
{
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");
throw new ApplicationException(
$"REPORT RENDER JOB {jobid} started but failed"
);
}
}
if (!done)
throw new TimeoutException("JOB FAILED DUE TO REPORT RENDER TIMEOUT");
throw new TimeoutException(
"JOB FAILED DUE TO REPORT RENDER TIMEOUT"
);
}
}
else
await m.SendEmailAsync(deliveryAddress, Subject, Body, ServerGlobalOpsSettingsCache.Notify);
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}";
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
@@ -305,7 +435,11 @@ namespace AyaNova.Biz
private static string SetQuoteTokens(string TheField, Quote qt, string CustomerName)
{
MatchCollection matches = Regex.Matches(TheField, @"\{{(.|\n)*?\}}", RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled);
MatchCollection matches = Regex.Matches(
TheField,
@"\{{(.|\n)*?\}}",
RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled
);
//{{.*?}}
foreach (Match KeyMatch in matches)
{
@@ -329,17 +463,24 @@ namespace AyaNova.Biz
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)
private static string SetWorkOrderTokens(
string TheField,
WorkOrder wo,
string CustomerName,
string statusName
)
{
MatchCollection matches = Regex.Matches(TheField, @"\{{(.|\n)*?\}}", RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled);
MatchCollection matches = Regex.Matches(
TheField,
@"\{{(.|\n)*?\}}",
RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled
);
//{{.*?}}
foreach (Match KeyMatch in matches)
{
@@ -363,17 +504,24 @@ namespace AyaNova.Biz
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)
private static string SetCSRTokens(
string TheField,
CustomerServiceRequest csr,
string CustomerName,
string requestedBy
)
{
MatchCollection matches = Regex.Matches(TheField, @"\{{(.|\n)*?\}}", RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled);
MatchCollection matches = Regex.Matches(
TheField,
@"\{{(.|\n)*?\}}",
RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled
);
//{{.*?}}
foreach (Match KeyMatch in matches)
{
@@ -407,9 +555,5 @@ namespace AyaNova.Biz
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons
} //eoc
} //eons