Compare commits
10 Commits
7cc59e329a
...
209298cfb2
| Author | SHA1 | Date | |
|---|---|---|---|
| 209298cfb2 | |||
| 9ac47ab052 | |||
| 98c7157b0c | |||
| ab75b98571 | |||
| 9d2a9af98a | |||
| 4c603cb648 | |||
| 76ffaa6910 | |||
| a12595b3cf | |||
| 8f2ab38ba2 | |||
| a296bc9b4f |
@@ -211,6 +211,9 @@ namespace Sockeye.Biz
|
||||
case 20:
|
||||
sb.AppendLine("Up to 20");
|
||||
break;
|
||||
case 25://case 4576
|
||||
sb.AppendLine("Up to 25");
|
||||
break;
|
||||
case 50:
|
||||
sb.AppendLine("Up to 50");
|
||||
break;
|
||||
|
||||
@@ -185,6 +185,7 @@ namespace Sockeye.Biz
|
||||
case "300740324"://PTI
|
||||
case "300740325"://OLI
|
||||
case "300807973"://Up to 15
|
||||
case "301091845"://Up to 25 //case 4576
|
||||
case "300740326"://Outlook Schedule Export
|
||||
case "300740316"://AyaNova LITE
|
||||
case "999"://Up to 999
|
||||
|
||||
@@ -590,23 +590,23 @@ namespace Sockeye.Biz
|
||||
|
||||
//Add Handlebars JS for compiling and presenting
|
||||
//https://handlebarsjs.com/
|
||||
await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "sock-hb.js") });
|
||||
await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "ay-hb.js") });
|
||||
|
||||
//add Marked for markdown processing
|
||||
//https://github.com/markedjs/marked
|
||||
await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "sock-md.js") });
|
||||
await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "ay-md.js") });
|
||||
|
||||
//add DOM Purify for markdown template sanitization processing
|
||||
//https://github.com/cure53/DOMPurify
|
||||
await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "sock-pf.js") });
|
||||
await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "ay-pf.js") });
|
||||
|
||||
//add Bar code library if our bar code helper is referenced
|
||||
//https://github.com/metafloor/bwip-js
|
||||
if (report.Template.Contains("ayBC ") || report.JsHelpers.Contains("ayBC "))
|
||||
await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "sock-bc.js") });
|
||||
await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "ay-bc.js") });
|
||||
|
||||
//add stock helpers
|
||||
await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "sock-report.js") });
|
||||
await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "ay-report.js") });
|
||||
|
||||
//execute to add to handlebars
|
||||
await page.EvaluateExpressionAsync("ayRegisterHelpers();");
|
||||
|
||||
@@ -66,11 +66,12 @@ namespace Sockeye.Biz
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//GET
|
||||
//
|
||||
internal async Task<Subscription> GetAsync(long id, bool logTheGetEvent = true, bool populateForReporting = false)
|
||||
internal async Task<Subscription> GetAsync(long id, bool logTheGetEvent = true)
|
||||
{
|
||||
var ret = await ct.Subscription.Include(z => z.Items.OrderByDescending(x=>x.Active).ThenBy(x => x.ExpireDate)).AsNoTracking().SingleOrDefaultAsync(z => z.Id == id);
|
||||
var ret = await ct.Subscription.Include(z => z.Items.OrderByDescending(x => x.Active).ThenBy(x => x.ExpireDate)).AsNoTracking().SingleOrDefaultAsync(z => z.Id == id);
|
||||
if (logTheGetEvent && ret != null)
|
||||
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, BizType, SockEvent.Retrieved), ct);
|
||||
|
||||
await PopulateVizFields(ret);
|
||||
return ret;
|
||||
}
|
||||
@@ -237,45 +238,88 @@ namespace Sockeye.Biz
|
||||
}
|
||||
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//REPORTING
|
||||
//
|
||||
public async Task<JArray> GetReportData(DataListSelectedRequest dataListSelectedRequest, Guid jobId)
|
||||
{
|
||||
if (dataListSelectedRequest.SockType == SockType.Subscription)
|
||||
return await GetSubscriptionsReportData(dataListSelectedRequest, jobId);
|
||||
else
|
||||
//subscription items
|
||||
return await GetSubscriptionItemsReportData(dataListSelectedRequest, jobId);
|
||||
}
|
||||
|
||||
|
||||
public async Task<JArray> GetSubscriptionsReportData(DataListSelectedRequest dataListSelectedRequest, Guid jobId)
|
||||
{
|
||||
var idList = dataListSelectedRequest.SelectedRowIds;
|
||||
JArray ReportData = new JArray();
|
||||
|
||||
List<Subscription> batchResults = new List<Subscription>();
|
||||
while (idList.Any())
|
||||
{
|
||||
if (!ReportRenderManager.KeepGoing(jobId)) return null;
|
||||
|
||||
var batch = idList.Take(IReportAbleObject.REPORT_DATA_BATCH_SIZE);
|
||||
idList = idList.Skip(IReportAbleObject.REPORT_DATA_BATCH_SIZE).ToArray();
|
||||
|
||||
//query for this batch, comes back in db natural order unfortunately
|
||||
var batchResults = await ct.Subscription.AsNoTracking().Where(z => batch.Contains(z.Id)).ToArrayAsync();
|
||||
|
||||
//order the results back into original
|
||||
//What is happening here:
|
||||
//for performance the query is batching a bunch at once by fetching a block of items from the sql server
|
||||
//however it's returning in db order which is often not the order the id list is in
|
||||
//so it needs to be sorted back into the same order as the ide list
|
||||
//This would not be necessary if just fetching each one at a time individually (like in workorder get report data)
|
||||
|
||||
var orderedList = from id in batch join z in batchResults on id equals z.Id select z;
|
||||
batchResults = null;
|
||||
|
||||
foreach (Subscription w in orderedList)
|
||||
batchResults.Clear();
|
||||
foreach (long batchId in batch)
|
||||
{
|
||||
if (!ReportRenderManager.KeepGoing(jobId)) return null;
|
||||
//var subId = await ct.SubscriptionItem.AsNoTracking().Where(z => z.Id == id).Select(z => z.SubscriptionId).FirstOrDefaultAsync();
|
||||
batchResults.Add(await GetAsync(batchId, false));
|
||||
}
|
||||
|
||||
//these are individually fetched so there's no need to re-order like most other object types
|
||||
|
||||
foreach (Subscription w in batchResults)
|
||||
{
|
||||
if (!ReportRenderManager.KeepGoing(jobId)) return null;
|
||||
await PopulateVizFields(w);
|
||||
var jo = JObject.FromObject(w);
|
||||
if (!JsonUtil.JTokenIsNullOrEmpty(jo["CustomFields"]))
|
||||
jo["CustomFields"] = JObject.Parse((string)jo["CustomFields"]);
|
||||
ReportData.Add(jo);
|
||||
}
|
||||
orderedList = null;
|
||||
}
|
||||
vc.Clear();
|
||||
return ReportData;
|
||||
}
|
||||
|
||||
public async Task<JArray> GetSubscriptionItemsReportData(DataListSelectedRequest dataListSelectedRequest, Guid jobId)
|
||||
{
|
||||
var idList = dataListSelectedRequest.SelectedRowIds;
|
||||
JArray ReportData = new JArray();
|
||||
|
||||
List<SubscriptionItem> batchResults = new List<SubscriptionItem>();
|
||||
while (idList.Any())
|
||||
{
|
||||
if (!ReportRenderManager.KeepGoing(jobId)) return null;
|
||||
|
||||
var batch = idList.Take(IReportAbleObject.REPORT_DATA_BATCH_SIZE);
|
||||
idList = idList.Skip(IReportAbleObject.REPORT_DATA_BATCH_SIZE).ToArray();
|
||||
batchResults.Clear();
|
||||
foreach (long batchId in batch)
|
||||
{
|
||||
if (!ReportRenderManager.KeepGoing(jobId)) return null;
|
||||
//var subId = .Select(z => z.SubscriptionId).FirstOrDefaultAsync();
|
||||
var subItem = await ct.SubscriptionItem.AsNoTracking().Where(z => z.Id == batchId).FirstOrDefaultAsync();
|
||||
await PopulateItemVizFields(subItem);
|
||||
batchResults.Add(subItem);
|
||||
}
|
||||
|
||||
//these are individually fetched so there's no need to re-order like most other object types
|
||||
|
||||
foreach (SubscriptionItem w in batchResults)
|
||||
{
|
||||
if (!ReportRenderManager.KeepGoing(jobId)) return null;
|
||||
var jo = JObject.FromObject(w);
|
||||
ReportData.Add(jo);
|
||||
}
|
||||
}
|
||||
vc.Clear();
|
||||
return ReportData;
|
||||
}
|
||||
|
||||
private VizCache vc = new VizCache();
|
||||
|
||||
|
||||
@@ -290,14 +334,52 @@ namespace Sockeye.Biz
|
||||
|
||||
foreach (var item in o.Items)//some subscriptions have a bunch of the same monthly or yearly raven user in them so this will save in that case
|
||||
{
|
||||
if (!vc.Has("productname", item.ProductId))
|
||||
{
|
||||
vc.Add(await ct.Product.AsNoTracking().Where(x => x.Id == item.ProductId).Select(x => x.Name).FirstOrDefaultAsync(), "productname", item.ProductId);
|
||||
}
|
||||
item.ProductViz = vc.Get("productname", item.ProductId);
|
||||
await PopulateItemVizFields(item, o);
|
||||
// if (!vc.Has("productname", item.ProductId))
|
||||
// {
|
||||
// var productInfo = await ct.Product.AsNoTracking().Where(x => x.Id == item.ProductId).FirstOrDefaultAsync();
|
||||
// vc.Add(productInfo.Name, "productname", item.ProductId);
|
||||
// vc.Add(productInfo.InitialPrice.ToString(), "productinitialprice", item.ProductId);
|
||||
// vc.Add(productInfo.RenewPrice.ToString(), "productrenewprice", item.ProductId);
|
||||
|
||||
// }
|
||||
// item.ProductViz = vc.Get("productname", item.ProductId);
|
||||
// item.RenewPriceViz = vc.GetAsDecimal("productrenewprice", item.ProductId) ?? 0;
|
||||
// item.InitialPriceViz = vc.GetAsDecimal("productinitialprice", item.ProductId) ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
//populate viz fields from provided object
|
||||
private async Task PopulateItemVizFields(SubscriptionItem o, Subscription sub = null)
|
||||
{
|
||||
|
||||
if (sub == null)
|
||||
sub = await ct.Subscription.AsNoTracking().Where(z => z.Id == o.SubscriptionId).FirstOrDefaultAsync();
|
||||
o.SubscriptionEmailViz = sub.FetchEmail;
|
||||
|
||||
if (!vc.Has("customername", sub.CustomerId))
|
||||
{
|
||||
var custInfo = await ct.Customer.AsNoTracking().Where(x => x.Id == sub.CustomerId).FirstOrDefaultAsync();
|
||||
vc.Add(custInfo.Name, "customername", sub.CustomerId);
|
||||
vc.Add(custInfo.EmailAddress, "customeremail", sub.CustomerId);
|
||||
}
|
||||
o.CustomerViz = vc.Get("customername", sub.CustomerId);
|
||||
o.CustomerEmailViz = vc.Get("customeremail", sub.CustomerId);
|
||||
|
||||
|
||||
if (!vc.Has("productname", o.ProductId))
|
||||
{
|
||||
var productInfo = await ct.Product.AsNoTracking().Where(x => x.Id == o.ProductId).FirstOrDefaultAsync();
|
||||
vc.Add(productInfo.Name, "productname", o.ProductId);
|
||||
vc.Add(productInfo.InitialPrice.ToString(), "productinitialprice", o.ProductId);
|
||||
vc.Add(productInfo.RenewPrice.ToString(), "productrenewprice", o.ProductId);
|
||||
}
|
||||
o.ProductViz = vc.Get("productname", o.ProductId);
|
||||
o.RenewPriceViz = vc.GetAsDecimal("productrenewprice", o.ProductId) ?? 0;
|
||||
o.InitialPriceViz = vc.GetAsDecimal("productinitialprice", o.ProductId) ?? 0;
|
||||
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// IMPORT EXPORT
|
||||
//
|
||||
|
||||
@@ -121,7 +121,7 @@ namespace Sockeye.Biz
|
||||
License newLicense = new License();
|
||||
//Get last license if any, set up some basic stuff common to all license types
|
||||
var firstPurchase = purchaseGroup.First();
|
||||
License lastLicense=null;
|
||||
License lastLicense = null;
|
||||
|
||||
newLicense.CustomerId = firstPurchase.CustomerId;
|
||||
newLicense.Active = false;
|
||||
@@ -131,7 +131,7 @@ namespace Sockeye.Biz
|
||||
//if there is one and it's not entirely expired then duplicate and fixup from purchases in this group
|
||||
if (isV7)
|
||||
{
|
||||
lastLicense = await ct.License.AsNoTracking().OrderByDescending(z => z.Id).FirstOrDefaultAsync(z => z.CustomerId == firstPurchase.CustomerId && z.PGroup == firstPurchase.PGroup);
|
||||
lastLicense = await ct.License.AsNoTracking().OrderByDescending(z => z.Id).FirstOrDefaultAsync(z => z.CustomerId == firstPurchase.CustomerId && z.PGroup == firstPurchase.PGroup);
|
||||
|
||||
newLicense.FetchEmail = purchaseGroupCustomer.EmailAddress;
|
||||
newLicense.PGroup = ProductGroup.AyaNova7;
|
||||
@@ -207,6 +207,8 @@ namespace Sockeye.Biz
|
||||
21 Single AyaNova service techncian perpetual license 301028314 135 100
|
||||
22 Single AyaNova service techncian 1 year maintenance plan - new 301028317 135 100
|
||||
23 Single AyaNova service techncian 1 year maintenance plan - active 301028315 100 100
|
||||
case 4576
|
||||
25 Up to 25 AyaNova schedulable resource 1 year subscription license
|
||||
*/
|
||||
var dtOneYear = DateTime.UtcNow.AddYears(1);
|
||||
switch (product.VendorCode)
|
||||
@@ -280,6 +282,10 @@ namespace Sockeye.Biz
|
||||
newLicense.Users = 15;
|
||||
newLicense.MaintenanceExpire = dtOneYear;
|
||||
break;
|
||||
case "301091845": //case 4576
|
||||
newLicense.Users = 25;
|
||||
newLicense.MaintenanceExpire = dtOneYear;
|
||||
break;
|
||||
default:
|
||||
var err = $"SockBotProcessPurchasesIntoLicenses purchase: {purchase.Id} has product not part of v7 group expected: {product.Name}-{product.VendorCode}";
|
||||
//serious issue requires immediate notification
|
||||
@@ -299,19 +305,19 @@ namespace Sockeye.Biz
|
||||
//it's a RAVEN license
|
||||
|
||||
|
||||
//get last license for this dbid, if active then take all it's value and update it with this
|
||||
lastLicense = await ct.License.AsNoTracking().OrderByDescending(z => z.Id).FirstOrDefaultAsync(z => z.CustomerId == firstPurchase.CustomerId && z.PGroup == firstPurchase.PGroup);
|
||||
//get last license for this dbid, if active then take all it's value and update it with this
|
||||
lastLicense = await ct.License.AsNoTracking().OrderByDescending(z => z.Id).FirstOrDefaultAsync(z => z.CustomerId == firstPurchase.CustomerId && z.PGroup == firstPurchase.PGroup);
|
||||
|
||||
|
||||
//If renewal and there is no last license then it's a problem -> THROW EXCEPTION
|
||||
//If renewal and there is no last license then it's a problem -> THROW EXCEPTION
|
||||
|
||||
//If no last license and not renewal then it's a new purchase -> GENERATE NEW
|
||||
//If no last license and not renewal then it's a new purchase -> GENERATE NEW
|
||||
|
||||
//If last license and renewal then it's a renewal -> UPDATE LAST LICENSE DATES SAVE NEW
|
||||
//If last license and renewal then it's a renewal -> UPDATE LAST LICENSE DATES SAVE NEW
|
||||
|
||||
//If last license and NEW then it's an additional add on count
|
||||
//if dbid matches -> ADD TO COUNT PUT THAT IN NOTES OF LICENSE
|
||||
//if no dbid match then -> THROW EXCEPTION
|
||||
//If last license and NEW then it's an additional add on count
|
||||
//if dbid matches -> ADD TO COUNT PUT THAT IN NOTES OF LICENSE
|
||||
//if no dbid match then -> THROW EXCEPTION
|
||||
|
||||
//iterate the purchases and update / set the license
|
||||
foreach (var purchase in purchaseGroup)
|
||||
@@ -334,10 +340,10 @@ namespace Sockeye.Biz
|
||||
301033168 AyaNova subscription additional 250 customer users yearly 250customerusersyearly Product 12 months HD1 Oct 6, 2022, 12:59 AM
|
||||
*/
|
||||
|
||||
// - sockeye note: Need to handle multiple raven license purchases, my policy is the freshest date is honoured so for examle tri-star bought in December then in Feb 2 users each time so the feb is the freshest, the trick is when the december renews not to cut it short but still use the feb date. since eventually sockeye will be automated it needs to handle this up front now, not later
|
||||
//algorithm: check if existing license for db id, if yes, is it for more users than current license?
|
||||
//if yes then is it unexpired?
|
||||
//if yes then honour the
|
||||
// - sockeye note: Need to handle multiple raven license purchases, my policy is the freshest date is honoured so for examle tri-star bought in December then in Feb 2 users each time so the feb is the freshest, the trick is when the december renews not to cut it short but still use the feb date. since eventually sockeye will be automated it needs to handle this up front now, not later
|
||||
//algorithm: check if existing license for db id, if yes, is it for more users than current license?
|
||||
//if yes then is it unexpired?
|
||||
//if yes then honour the
|
||||
//RAVEN licenses have one week padding to be on the safe side
|
||||
var dtOneYear = DateTime.UtcNow.AddYears(1).AddDays(7);
|
||||
var dtOneMonth = DateTime.UtcNow.AddMonths(1).AddDays(7);
|
||||
|
||||
@@ -15,8 +15,13 @@ namespace Sockeye.Models
|
||||
[Required]
|
||||
public string Name { get; set; }
|
||||
public bool Active { get; set; }
|
||||
|
||||
public decimal InitialPrice { get; set; } = 0;
|
||||
public decimal RenewPrice { get; set; } = 0;
|
||||
|
||||
|
||||
[Required]
|
||||
public ProductGroup PGroup {get;set;}
|
||||
public ProductGroup PGroup { get; set; }
|
||||
public long VendorId { get; set; }
|
||||
public TimeSpan LicenseInterval { get; set; }
|
||||
public TimeSpan MaintInterval { get; set; }
|
||||
|
||||
@@ -19,8 +19,7 @@ namespace Sockeye.Models
|
||||
public bool Active { get; set; }
|
||||
[Required]
|
||||
public long ProductId { get; set; }
|
||||
[NotMapped]
|
||||
public string ProductViz { get; set; }
|
||||
|
||||
[Required]
|
||||
public DateTime ExpireDate { get; set; }
|
||||
[Required]
|
||||
@@ -30,6 +29,23 @@ namespace Sockeye.Models
|
||||
[Required]
|
||||
public DateTime OriginalOrderDate { get; set; }
|
||||
|
||||
public bool Renewal { get; set; }
|
||||
|
||||
[NotMapped]
|
||||
public decimal RenewPriceViz { get; set; } = 0;
|
||||
[NotMapped]
|
||||
public decimal InitialPriceViz { get; set; } = 0;
|
||||
|
||||
[NotMapped]
|
||||
public string ProductViz { get; set; }
|
||||
|
||||
[NotMapped]
|
||||
public string CustomerViz { get; set; }
|
||||
[NotMapped]
|
||||
public string CustomerEmailViz { get; set; }
|
||||
[NotMapped]
|
||||
public string SubscriptionEmailViz { get; set; }
|
||||
|
||||
|
||||
public List<string> Tags { get; set; } = new List<string>();
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<GenerateFullPaths>true</GenerateFullPaths>
|
||||
<Version>8.0.13</Version>
|
||||
<FileVersion>8.0.13.0</FileVersion>
|
||||
<Version>8.0.16</Version>
|
||||
<FileVersion>8.0.16.0</FileVersion>
|
||||
<ApplicationIcon>sockeye.ico</ApplicationIcon>
|
||||
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
||||
<noWarn>1591</noWarn>
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace Sockeye.Util
|
||||
/// </summary>
|
||||
internal static class SockeyeVersion
|
||||
{
|
||||
public const string VersionString = "8.0.13";
|
||||
public const string VersionString = "8.0.17";
|
||||
public const string FullNameAndVersion = "Sockeye server " + VersionString;
|
||||
public const string CurrentApiVersion="v8";
|
||||
}//eoc
|
||||
|
||||
10
todo.txt
10
todo.txt
@@ -15,18 +15,16 @@ TODO:
|
||||
|
||||
TODO: make a filter showing expired but not set to inactive subscription items
|
||||
|
||||
TODO: dashboard items related to subscriptions
|
||||
- revenue per month for next 12 months graph
|
||||
- overdue expired subs list
|
||||
- active count of each product subscription bar graph
|
||||
|
||||
todo: build email address list by active subscription product no dupes
|
||||
maybe in a report?
|
||||
This is for alerting subscribers to update or non subscribers to update separately
|
||||
maybe it's a full on feature?
|
||||
|
||||
|
||||
|
||||
TODO: dashboard items related to subscriptions
|
||||
- revenue per month for next 12 months graph
|
||||
- overdue expired subs list
|
||||
- active count of each product subscription bar graph
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user