diff --git a/server/generator/SockBotProcessPurchasesIntoLicenses.cs b/server/generator/SockBotProcessPurchasesIntoLicenses.cs
new file mode 100644
index 0000000..016dfc1
--- /dev/null
+++ b/server/generator/SockBotProcessPurchasesIntoLicenses.cs
@@ -0,0 +1,1235 @@
+using System;
+using System.Linq;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using Newtonsoft.Json.Linq;
+using Sockeye.Models;
+using Sockeye.Util;
+
+namespace Sockeye.Biz
+{
+
+
+ ///
+ /// Process purchases into licenses
+ ///
+ ///
+ internal static class SockBotProcessPurchasesIntoLicenses
+ {
+ private static ILogger log = Sockeye.Util.ApplicationLogging.CreateLogger("SockBotProcessPurchasesIntoLicenses");
+ private static DateTime lastSweep = DateTime.MinValue;
+#if (DEBUG)
+ private static TimeSpan PROCESS_EVERY_INTERVAL = new TimeSpan(0, 0, 30);//every 30 seconds during development
+#else
+ private static TimeSpan PROCESS_EVERY_INTERVAL = new TimeSpan(0, 5, 10);//every 5 minutes
+#endif
+ ////////////////////////////////////////////////////////////////////////////////////////////////
+ // DoSweep
+ //
+ public static async Task DoWorkAsync()
+ {
+ //This will get triggered roughly every minute, but we don't want to check that frequently
+ if (DateTime.UtcNow - lastSweep < PROCESS_EVERY_INTERVAL)
+ return;
+
+ if (ServerBootConfig.MIGRATING) return;//don't do this during migration (migration is one time only so can remove after up and running)
+
+ log.LogDebug("Process purchases into licenses starting");
+ await ProcessPurchasesIntoLicenses();
+
+ lastSweep = DateTime.UtcNow;
+ }
+
+ private static async Task ProcessPurchasesIntoLicenses()
+ {
+ using (AyContext ct = Sockeye.Util.ServiceProviderProvider.DBContext)
+ {
+
+ //get a list of all actionable purchases
+ var purchaseList = await ct.Purchase
+ .Where(z => z.Processed == false
+ && z.LicenseId == null
+ && z.CustomerId != null
+ && (z.PGroup == ProductGroup.AyaNova7 || z.PGroup == ProductGroup.RavenPerpetual || z.PGroup == ProductGroup.RavenSubscription))
+ .OrderBy(z => z.Id)
+ .ToListAsync();
+
+ try
+ {
+ foreach (var p in purchaseList)
+ {
+ log.LogDebug($"Processing purchase id:{p.Id},purchasedate:{p.PurchaseDate}, custid:{p.CustomerId}");
+
+ //if v7 product group the purchase must be more than 5 minutes old to account for mycommerce trickling in add-on bits to a v7 order
+ //(I went over old sales, mycommerce has at most 2 minutes variance in multiple item order on same day with the exception of unusual situations like where part
+ //of an order was on an invalid credit card or not updated in one case, another was a refund and purchase of alternate item etc)
+ //So in the normal course of things going to assume the best and check further down if something seems missing from last license
+
+
+ if (string.IsNullOrWhiteSpace(p.VendorData))
+ {
+ var err = $"VendorNotification record {p.Id}-{p.Created} has no vendor data";
+ await NotifyEventHelper.AddOpsProblemEvent("SockBotProcessPurchasesIntoLicenses: " + err);
+ log.LogError(err);
+ continue;
+ }
+ //Parse json vendordata
+ if (await ParseVendorNotificationData(p, ct, log))
+ {
+ //success, save vendornotification as processed
+ p.Processed = DateTime.UtcNow;
+ await ct.SaveChangesAsync();
+ }
+
+ }
+ }
+ catch (Exception ex)
+ {
+ var err = "SockBotProcessPurchasesIntoLicenses error running job";
+ //serious issue requires immediate notification
+ await NotifyEventHelper.AddOpsProblemEvent(err, ex);
+ log.LogError(ex, err);
+ }
+
+ }
+ }
+
+
+ internal static async Task ParseVendorNotificationData(VendorNotification vn, AyContext ct, ILogger log)
+ {
+
+ try
+ {
+ var jData = JObject.Parse(vn.VendorData);
+
+ //It's a test purchase, no need to process it any further...or is there??
+ bool IsTestOrder = false;
+ if (jData["order_notification"]["purchase"]["is_test"] != null && jData["order_notification"]["purchase"]["is_test"].Value() == true)
+ IsTestOrder = true;
+
+ //fundamentally validate the object is a purchase notification
+ if (jData["order_notification"]["purchase"]["purchase_id"] == null)
+ {
+ //this is not the expected format data, stop processing and alert:
+ throw new System.FormatException($"Vendor data unexpected format:{vn.VendorData}");
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////
+ #region CUSTOMER MAKE OR LOCATED
+ ///////////////////////////////////////////////////////////////////////////////////////////
+ var jCustomerName = jData["order_notification"]["purchase"]["customer_data"]["reg_name"].Value() ?? throw new System.FormatException($"Vendor data empty reg_name:{vn.VendorData}");
+ if (jData["order_notification"]["purchase"]["customer_data"]["delivery_contact"]["email"] == null)//we can't process orders with no email at all hard no
+ throw new System.FormatException($"Vendor data empty email:{vn.VendorData}");
+ var jCustomerEmail = jData["order_notification"]["purchase"]["customer_data"]["delivery_contact"]["email"].Value();
+ var jCustomerAccountNumber = jData["order_notification"]["purchase"]["customer_data"]["shopper_id"].Value();//appears to be mycommerce customer id number hopefully static between orders
+
+ var customerBiz = CustomerBiz.GetBiz(ct);
+
+ //attempt to match to existing customer
+ //account number is most ideal match, name second but could be multiple in sockeye from rockfish sites so name will start the same, finally email if nothing else
+ Customer customer = await ct.Customer.AsNoTracking().FirstOrDefaultAsync(z => z.AccountNumber == jCustomerAccountNumber)
+ ?? await ct.Customer.AsNoTracking().FirstOrDefaultAsync(z => z.Name.StartsWith(jCustomerName))
+ ?? await ct.Customer.AsNoTracking().FirstOrDefaultAsync(z => z.EmailAddress == jCustomerEmail);
+ if (customer == null)
+ {
+ //New customer
+ customer = new Customer();
+ customer.Name = jCustomerName;
+ customer.EmailAddress = jCustomerEmail;
+ customer.AccountNumber = jCustomerAccountNumber;
+ UpdateCustomerFromVendorData(jData, customer);
+ customer = await customerBiz.CreateAsync(customer);
+ if (customer == null)
+ throw new System.ApplicationException($"Error creating new Customer: {customerBiz.GetErrorsAsString()} vendor data :{vn.VendorData}");
+ }
+ else
+ {
+ //existing customer
+ //here there could be several potential issues:
+ //name differs because it was a separate site in rockfish, it's not cool to change the name
+ //email differs, this can happen and is ok
+ //account number differs if was empty then it's ok. If it wasn't empty and it differs this is unfortunately normal as users may re-buy again with new account or buy an addon with a new account
+ //so the vendor account nubmer should just be the most recent for finding them purposes I guess
+ //
+ if (customer.EmailAddress != jCustomerEmail)
+ customer.EmailAddress = jCustomerEmail;//assume it was empty or has been recently updated
+
+ if (customer.AccountNumber != jCustomerAccountNumber)
+ customer.AccountNumber = jCustomerAccountNumber;//see above
+
+
+ //refresh
+ UpdateCustomerFromVendorData(jData, customer);
+ customer = await customerBiz.PutAsync(customer);
+ if (customer == null)
+ throw new System.ApplicationException($"Error updating existing Customer: {customerBiz.GetErrorsAsString()} vendor data :{vn.VendorData}");
+ }
+ ///////////////////////////////////////////////////////////////////////////////////////////
+ #endregion customer make or locate
+
+ ///////////////////////////////////////////////////////////////////////////////////////////
+ #region MAKE PURCHASE RECORD
+ ///////////////////////////////////////////////////////////////////////////////////////////
+
+ var salesOrderNumber = jData["order_notification"]["purchase"]["purchase_id"].Value();
+ if (IsTestOrder)
+ salesOrderNumber = "test-order-" + salesOrderNumber;
+#if (DEBUG)
+ salesOrderNumber += "-debug-test";
+#endif
+
+ //https://www.newtonsoft.com/json/help/html/DatesInJSON.htm
+ var purchaseDate = jData["order_notification"]["purchase"]["purchase_date"].Value();
+
+ if (await ct.Purchase.AnyAsync(z => z.SalesOrderNumber == salesOrderNumber))
+ throw new System.ApplicationException($"Sales order already exists: {salesOrderNumber} will not be processed");
+
+ //iterate purchase items array
+ var jaPurchaseList = (JArray)jData["order_notification"]["purchase"]["purchase_item"];
+
+ foreach (JObject jPurchase in jaPurchaseList)
+ {
+ Purchase p = new Purchase();
+ p.PurchaseDate = purchaseDate;
+ p.CustomerId = customer.Id;
+ p.VendorId = vn.VendorId;
+ p.SalesOrderNumber = salesOrderNumber;
+ var SalesItemProductVendorCode = jPurchase["product_id"].Value();
+ var product = await ct.Product.AsNoTracking().FirstOrDefaultAsync(z => z.VendorCode == SalesItemProductVendorCode) ?? throw new System.ArgumentOutOfRangeException($"Vendor product code:{SalesItemProductVendorCode} was not found in Sockeye Products, record not processed");
+ p.ProductId = product.Id;
+ p.PGroup = product.PGroup;
+ if (jPurchase["accounting"] != null)
+ {
+ p.Currency = jPurchase["accounting"]["currency"].Value();
+ p.ProductNet = jPurchase["accounting"]["product_net"].Value();
+ p.Discount = jPurchase["accounting"]["discount"].Value();
+ p.VendorFee = jPurchase["accounting"]["margin_net"].Value();
+ p.Revenue = jPurchase["accounting"]["your_revenue"].Value();
+ }
+
+ //Capture raven database id if present
+ if (jPurchase["additional_information"] != null)
+ {
+ var jaAdditionalItems = (JArray)jPurchase["additional_information"];
+ foreach (JObject jAdditionalItem in jaAdditionalItems)
+ {
+ if (jAdditionalItem["additional_id"] != null && jAdditionalItem["additional_id"].Value() == "DATABASEID")
+ {
+ p.DbId = jAdditionalItem["additional_value"].Value();
+ break;
+ }
+ }
+ }
+
+ if (jPurchase["promotion_coupon"] != null)
+ p.CouponCode = jPurchase["promotion_coupon"].Value();
+
+ if (jPurchase["promotion"] != null)
+ p.CouponCode += $" {jPurchase["promotion"].Value()}";
+
+ p.Quantity = jPurchase["quantity"].Value();
+ p.VendorNotificationId = vn.Id;
+
+ //it's a subscription?
+ if (jPurchase["subscription"] != null && jPurchase["subscription"]["expiration_date"] != null)
+ {
+ p.ExpireDate = jPurchase["subscription"]["expiration_date"].Value();
+ }
+
+ PurchaseBiz pbiz = PurchaseBiz.GetBiz(ct);
+ p = await pbiz.CreateAsync(p);
+ if (p == null)
+ {
+ //did not save, throw an error
+ throw new System.ApplicationException($"Error creating purchase: {pbiz.GetErrorsAsString()} for product item: {SalesItemProductVendorCode} vendor data :{vn.VendorData}");
+ }
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////
+ #endregion make purchase record
+ }
+ catch (Exception ex)
+ {
+ var err = $"ParseVendorNotificationData: VendorNotification record {vn.Id}-{vn.Created} triggered exception, see log";
+ await NotifyEventHelper.AddOpsProblemEvent(err);//notify, this is serious
+ log.LogError(ex, err);
+ return false;
+ }
+ return true; //successfully processed so vendor notification should be set to processed by caller
+ }
+
+ private static void UpdateCustomerFromVendorData(JObject jData, Customer c)
+ {
+ c.Active = true;//if they just made a purchase they are active even if they weren't before
+ c.DoNotContact = false;//if they just made a purchase they are contactable even if they weren't before
+ c.Address = c.PostAddress = jData["order_notification"]["purchase"]["customer_data"]["delivery_contact"]["address"]["street1"].Value();
+ c.City = c.PostCity = jData["order_notification"]["purchase"]["customer_data"]["delivery_contact"]["address"]["city"].Value();
+
+ //State doesn't always exist in mycommerce notifications
+ if (jData["order_notification"]["purchase"]["customer_data"]["delivery_contact"]["address"]["state"] != null)
+ c.Region = c.PostRegion = jData["order_notification"]["purchase"]["customer_data"]["delivery_contact"]["address"]["state"].Value();
+ c.Country = c.PostCountry = jData["order_notification"]["purchase"]["customer_data"]["delivery_contact"]["address"]["country"].Value();
+ c.PostCode = c.AddressPostal = jData["order_notification"]["purchase"]["customer_data"]["delivery_contact"]["address"]["postal_code"].Value();
+ var firstName = jData["order_notification"]["purchase"]["customer_data"]["delivery_contact"]["first_name"].Value() ?? "FirstNameEmpty";
+ var lastName = jData["order_notification"]["purchase"]["customer_data"]["delivery_contact"]["last_name"].Value() ?? "LastNameEmpty";
+ var language = jData["order_notification"]["purchase"]["customer_data"]["language"].Value() ?? "LanguageEmpty";
+ if (string.IsNullOrWhiteSpace(c.Notes))
+ c.Notes = string.Empty;
+ if (!c.Notes.Contains(lastName))
+ {
+ if (c.Notes.Length > 0 && !c.Notes.EndsWith('\n'))
+ c.Notes += "\n";
+ c.Notes += $"Purchase contact:{firstName} {lastName}, language: {language}\n";
+ }
+
+ }
+
+
+ #region SAMPLE VENDOR PURCHASE NOTIFICATIONS
+ /*
+
+
+/////////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////// V7 MULTIPLE PRODUCTS IN ONE ORDER/////////////////////////////
+
+{
+ "creation_date": "2023-01-22T23:21:49Z",
+ "id": 357678128,
+ "order_notification": {
+ "purchase": {
+ "customer_data": {
+ "billing_contact": {
+ "address": {
+ "city": "Courtenay",
+ "country": "Canada",
+ "country_id": "CA",
+ "postal_code": "V9J9T6",
+ "state": "British Columbia",
+ "state_id": "BC",
+ "street1": "05-3610 Christie Parkway"
+ },
+ "company": "GZTestCo",
+ "email": "gzmailadmin@gmail.com",
+ "first_name": "Test",
+ "last_name": "Testerson"
+ },
+ "customer_payment_data": {
+ "currency": "USD",
+ "payment_method": "Other"
+ },
+ "delivery_contact": {
+ "address": {
+ "city": "Courtenay",
+ "country": "Canada",
+ "country_id": "CA",
+ "postal_code": "V9J9T6",
+ "state": "British Columbia",
+ "state_id": "BC",
+ "street1": "05-3610 Christie Parkway"
+ },
+ "company": "GZTestCo",
+ "email": "gzmailadmin@gmail.com",
+ "first_name": "Test",
+ "last_name": "Testerson"
+ },
+ "language": "English",
+ "language_iso": "en",
+ "reg_name": "GZTestCo",
+ "shopper_id": "65860321",
+ "subscribe_newsletter": false,
+ "user_id": "gzmailadmin@gmail.com-38"
+ },
+ "is_test": true,
+ "payment_complete_date": "2023-01-22T23:21:48Z",
+ "payment_status": "testpaymentarrived",
+ "payment_status_id": "TCA",
+ "purchase_date": "2023-01-22T23:21:47Z",
+ "purchase_id": 843671213,
+ "purchase_item": [
+ {
+ "additional_information": [
+ {
+ "additional_id": "ADDITIONAL1",
+ "additional_value": "YES"
+ },
+ {
+ "additional_id": "ADDITIONAL2",
+ "additional_value": "YES"
+ },
+ {
+ "additional_id": "ADDITIONAL3",
+ "additional_value": "YES"
+ }
+ ],
+ "currency": "USD",
+ "delivery_type": "Electronically",
+ "discount": 0.0,
+ "extended_download_price": 0.0,
+ "manual_order_price": 0.0,
+ "notification_no": 0,
+ "product_id": 300740317,
+ "product_name": "Up to 5 AyaNova schedulable resource 1 year subscription license",
+ "product_single_price": 695.0,
+ "purchase_item_key": [],
+ "quantity": 1,
+ "running_no": 1,
+ "shipping_price": 0.0,
+ "shipping_vat_pct": 0.0,
+ "subscription": {
+ "expiration_date": "2024-01-22T23:21:48Z",
+ "id": "843671213-1",
+ "interval": "Yearly without end",
+ "renewal_discount_count": "",
+ "renewal_discount_start": "",
+ "renewal_type": "auto",
+ "retention_discount_count": "",
+ "retention_discount_percent": "",
+ "start_date": "2023-01-23T00:00:00",
+ "status": "ToProcess",
+ "status_id": "TOP"
+ },
+ "vat_pct": 12.0
+ },
+ {
+ "additional_information": [
+ {
+ "additional_id": "ADDITIONAL1",
+ "additional_value": "YES"
+ },
+ {
+ "additional_id": "ADDITIONAL2",
+ "additional_value": "YES"
+ },
+ {
+ "additional_id": "ADDITIONAL3",
+ "additional_value": "YES"
+ }
+ ],
+ "currency": "USD",
+ "delivery_type": "Electronically",
+ "discount": 0.0,
+ "extended_download_price": 0.0,
+ "manual_order_price": 0.0,
+ "notification_no": 0,
+ "product_id": 300740314,
+ "product_name": "optional add-on AyaNova RI (responsive interface) 1 year subscription license",
+ "product_single_price": 199.0,
+ "purchase_item_key": [],
+ "quantity": 1,
+ "running_no": 2,
+ "shipping_price": 0.0,
+ "shipping_vat_pct": 0.0,
+ "subscription": {
+ "expiration_date": "2024-01-22T23:21:48Z",
+ "id": "843671213-2",
+ "interval": "Yearly without end",
+ "renewal_discount_count": "",
+ "renewal_discount_start": "",
+ "renewal_type": "auto",
+ "retention_discount_count": "",
+ "retention_discount_percent": "",
+ "start_date": "2023-01-23T00:00:00",
+ "status": "ToProcess",
+ "status_id": "TOP"
+ },
+ "vat_pct": 12.0
+ },
+ {
+ "additional_information": [
+ {
+ "additional_id": "ADDITIONAL1",
+ "additional_value": "YES"
+ }
+ ],
+ "currency": "USD",
+ "delivery_type": "Electronically",
+ "discount": 0.0,
+ "extended_download_price": 0.0,
+ "manual_order_price": 0.0,
+ "notification_no": 0,
+ "product_id": 300740321,
+ "product_name": "optional add-on AyaNova WBI (web browser interface) 1 year subscription license",
+ "product_single_price": 99.0,
+ "purchase_item_key": [],
+ "quantity": 1,
+ "running_no": 3,
+ "shipping_price": 0.0,
+ "shipping_vat_pct": 0.0,
+ "subscription": {
+ "expiration_date": "2024-01-22T23:21:48Z",
+ "id": "843671213-3",
+ "interval": "Yearly without end",
+ "renewal_discount_count": "",
+ "renewal_discount_start": "",
+ "renewal_type": "auto",
+ "retention_discount_count": "",
+ "retention_discount_percent": "",
+ "start_date": "2023-01-23T00:00:00",
+ "status": "ToProcess",
+ "status_id": "TOP"
+ },
+ "vat_pct": 12.0
+ },
+ {
+ "additional_information": [
+ {
+ "additional_id": "ADDITIONAL1",
+ "additional_value": "YES"
+ }
+ ],
+ "currency": "USD",
+ "delivery_type": "Electronically",
+ "discount": 0.0,
+ "extended_download_price": 0.0,
+ "manual_order_price": 0.0,
+ "notification_no": 0,
+ "product_id": 300740322,
+ "product_name": "optional add-on AyaNova MBI (minimal browser interface) 1 year subscription license",
+ "product_single_price": 99.0,
+ "purchase_item_key": [],
+ "quantity": 1,
+ "running_no": 4,
+ "shipping_price": 0.0,
+ "shipping_vat_pct": 0.0,
+ "subscription": {
+ "expiration_date": "2024-01-22T23:21:48Z",
+ "id": "843671213-4",
+ "interval": "Yearly without end",
+ "renewal_discount_count": "",
+ "renewal_discount_start": "",
+ "renewal_type": "auto",
+ "retention_discount_count": "",
+ "retention_discount_percent": "",
+ "start_date": "2023-01-23T00:00:00",
+ "status": "ToProcess",
+ "status_id": "TOP"
+ },
+ "vat_pct": 12.0
+ },
+ {
+ "additional_information": [
+ {
+ "additional_id": "ADDITIONAL1",
+ "additional_value": "YES"
+ }
+ ],
+ "currency": "USD",
+ "delivery_type": "Electronically",
+ "discount": 0.0,
+ "extended_download_price": 0.0,
+ "manual_order_price": 0.0,
+ "notification_no": 0,
+ "product_id": 300740323,
+ "product_name": "optional add-on AyaNova QBI(QuickBooks interface) 1 year subscription license",
+ "product_single_price": 99.0,
+ "purchase_item_key": [],
+ "quantity": 1,
+ "running_no": 5,
+ "shipping_price": 0.0,
+ "shipping_vat_pct": 0.0,
+ "subscription": {
+ "expiration_date": "2024-01-22T23:21:48Z",
+ "id": "843671213-5",
+ "interval": "Yearly without end",
+ "renewal_discount_count": "",
+ "renewal_discount_start": "",
+ "renewal_type": "auto",
+ "retention_discount_count": "",
+ "retention_discount_percent": "",
+ "start_date": "2023-01-23T00:00:00",
+ "status": "ToProcess",
+ "status_id": "TOP"
+ },
+ "vat_pct": 12.0
+ }
+ ],
+ "purchase_origin": "online"
+ }
+ }
+}
+
+
+
+ /////////////////////////////////////////////////////////////////////////////////////////////////////
+ //////////////////////v7 export to xls add on/////////////////////////////
+ {
+"creation_date": "2023-01-20T00:18:33Z",
+"id": 357520816,
+"order_notification": {
+"purchase": {
+ "customer_data": {
+ "billing_contact": {
+ "address": {
+ "city": "Cambridge",
+ "country": "Canada",
+ "country_id": "CA",
+ "postal_code": "N1T 1J3",
+ "state": "Ontario",
+ "state_id": "ON",
+ "street1": "181 Shearson Crescent"
+ },
+ "company": "Cambridge Elevating Inc",
+ "email": "richard.wright@cambridgeelevating.com",
+ "first_name": "Richard",
+ "last_name": "Wright"
+ },
+ "customer_payment_data": {
+ "currency": "USD",
+ "payment_method": "MasterCard"
+ },
+ "delivery_contact": {
+ "address": {
+ "city": "Cambridge",
+ "country": "Canada",
+ "country_id": "CA",
+ "postal_code": "N1T 1J3",
+ "state": "Ontario",
+ "state_id": "ON",
+ "street1": "181 Shearson Crescent"
+ },
+ "company": "Cambridge Elevating Inc",
+ "email": "richard.wright@cambridgeelevating.com",
+ "first_name": "Richard",
+ "last_name": "Wright"
+ },
+ "language": "English",
+ "language_iso": "en",
+ "reg_name": "Cambridge Elevating Inc",
+ "shopper_id": "62545677",
+ "subscribe_newsletter": false,
+ "user_id": "richard.wright@cambridgeelevating.com-5"
+ },
+ "is_test": false,
+ "payment_complete_date": "2023-01-20T00:18:33Z",
+ "payment_status": "complete",
+ "payment_status_id": "PCA",
+ "purchase_date": "2023-01-20T00:18:31Z",
+ "purchase_id": 843101393,
+ "purchase_item": [
+ {
+ "accounting": {
+ "currency": "USD",
+ "product_net": 6.65,
+ "discount": 0.0,
+ "product_vat": 0.86,
+ "shipping": 0.0,
+ "shipping_vat": 0.0,
+ "eu_vat": -0.86,
+ "margin_net": -2.5,
+ "your_revenue": 4.15
+ },
+ "additional_information": [
+ {
+ "additional_id": "ADDITIONAL1",
+ "additional_value": "YES"
+ }
+ ],
+ "currency": "USD",
+ "delivery_type": "Electronically",
+ "discount": 0.0,
+ "extended_download_price": 0.0,
+ "manual_order_price": 0.0,
+ "notification_no": 7953,
+ "product_id": 300740327,
+ "product_name": "optional add-on plug-in Export to XLS 1 year subscription license",
+ "product_single_price": 6.65,
+ "purchase_item_key": [],
+ "quantity": 1,
+ "running_no": 1,
+ "shipping_price": 0.0,
+ "shipping_vat_pct": 0.0,
+ "subscription": {
+ "expiration_date": "2024-01-20T18:36:22Z",
+ "id": "771160083-3",
+ "interval": "Yearly without end",
+ "original_notification_no": "7779",
+ "original_purchase_id": "771160083",
+ "original_running_no": "3",
+ "renewal_discount_count": "",
+ "renewal_discount_start": "",
+ "renewal_type": "auto",
+ "retention_discount_count": "",
+ "retention_discount_percent": "",
+ "start_date": "2022-01-20T00:00:00",
+ "status": "ToProcess",
+ "status_id": "TOP"
+ },
+ "vat_pct": 13.0
+ }
+ ],
+ "purchase_origin": "Subscription",
+ "sequential_invoice_no": "e5-DE-2023-00000163106"
+}
+}
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////////////////
+///////////////////////////////////// V7 Scheduleable user //////////////////////
+
+{
+"creation_date": "2023-01-20T00:18:32Z",
+"id": 357520809,
+"order_notification": {
+"purchase": {
+ "customer_data": {
+ "billing_contact": {
+ "address": {
+ "city": "Cambridge",
+ "country": "Canada",
+ "country_id": "CA",
+ "postal_code": "N1T 1J3",
+ "state": "Ontario",
+ "state_id": "ON",
+ "street1": "181 Shearson Crescent"
+ },
+ "company": "Cambridge Elevating Inc",
+ "email": "richard.wright@cambridgeelevating.com",
+ "first_name": "Richard",
+ "last_name": "Wright"
+ },
+ "customer_payment_data": {
+ "currency": "USD",
+ "payment_method": "MasterCard"
+ },
+ "delivery_contact": {
+ "address": {
+ "city": "Cambridge",
+ "country": "Canada",
+ "country_id": "CA",
+ "postal_code": "N1T 1J3",
+ "state": "Ontario",
+ "state_id": "ON",
+ "street1": "181 Shearson Crescent"
+ },
+ "company": "Cambridge Elevating Inc",
+ "email": "richard.wright@cambridgeelevating.com",
+ "first_name": "Richard",
+ "last_name": "Wright"
+ },
+ "language": "English",
+ "language_iso": "en",
+ "reg_name": "Cambridge Elevating Inc",
+ "shopper_id": "62545677",
+ "subscribe_newsletter": false,
+ "user_id": "richard.wright@cambridgeelevating.com-5"
+ },
+ "is_test": false,
+ "payment_complete_date": "2023-01-20T00:18:32Z",
+ "payment_status": "complete",
+ "payment_status_id": "PCA",
+ "purchase_date": "2023-01-20T00:18:31Z",
+ "purchase_id": 843101383,
+ "purchase_item": [
+ {
+ "accounting": {
+ "currency": "USD",
+ "product_net": 577.5,
+ "discount": 0.0,
+ "product_vat": 75.08,
+ "shipping": 0.0,
+ "shipping_vat": 0.0,
+ "eu_vat": -75.08,
+ "margin_net": -29.05,
+ "your_revenue": 548.45
+ },
+ "additional_information": [
+ {
+ "additional_id": "ADDITIONAL1",
+ "additional_value": "YES"
+ },
+ {
+ "additional_id": "ADDITIONAL2",
+ "additional_value": "YES"
+ },
+ {
+ "additional_id": "ADDITIONAL3",
+ "additional_value": "YES"
+ }
+ ],
+ "currency": "USD",
+ "delivery_type": "Electronically",
+ "discount": 0.0,
+ "extended_download_price": 0.0,
+ "manual_order_price": 0.0,
+ "notification_no": 7952,
+ "product_id": 300807973,
+ "product_name": "Up to 15 AyaNova schedulable resource 1 year subscription license",
+ "product_single_price": 577.5,
+ "purchase_item_key": [],
+ "quantity": 1,
+ "running_no": 1,
+ "shipping_price": 0.0,
+ "shipping_vat_pct": 0.0,
+ "subscription": {
+ "expiration_date": "2024-01-20T18:36:22Z",
+ "id": "771160083-1",
+ "interval": "Yearly without end",
+ "original_notification_no": "7777",
+ "original_purchase_id": "771160083",
+ "original_running_no": "1",
+ "renewal_discount_count": "",
+ "renewal_discount_start": "",
+ "renewal_type": "auto",
+ "retention_discount_count": "",
+ "retention_discount_percent": "",
+ "start_date": "2022-01-20T00:00:00",
+ "status": "ToProcess",
+ "status_id": "TOP"
+ },
+ "vat_pct": 13.0
+ }
+ ],
+ "purchase_origin": "Subscription",
+ "sequential_invoice_no": "e5-DE-2023-00000163105"
+}
+}
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////////////////
+//////////////////// NOT A LICENSE PRODUCT - CUSTOMIZATION SERVICES FOR REPORT TEMPLATE
+
+{
+"creation_date": "2023-01-18T15:45:50Z",
+"id": 357446088,
+"order_notification": {
+"purchase": {
+ "customer_data": {
+ "billing_contact": {
+ "address": {
+ "city": "Brunswick",
+ "country": "USA",
+ "country_id": "US",
+ "postal_code": "38014",
+ "state": "Tennessee",
+ "state_id": "TN",
+ "street1": "PO Box 374"
+ },
+ "company": "Tri-Star Medical Technologies LLC",
+ "email": "stan@tri-starmedical.com",
+ "first_name": "Stan",
+ "last_name": "Hilton"
+ },
+ "customer_payment_data": {
+ "currency": "USD",
+ "payment_method": "Visa"
+ },
+ "delivery_contact": {
+ "address": {
+ "city": "Brunswick",
+ "country": "USA",
+ "country_id": "US",
+ "postal_code": "38014",
+ "state": "Tennessee",
+ "state_id": "TN",
+ "street1": "PO Box 374"
+ },
+ "company": "Tri-Star Medical Technologies LLC",
+ "email": "stan@tri-starmedical.com",
+ "first_name": "Stan",
+ "last_name": "Hilton"
+ },
+ "language": "English",
+ "language_iso": "en",
+ "reg_name": "Tri-Star Medical Technologies LLC",
+ "shopper_id": "65821033",
+ "subscribe_newsletter": false,
+ "user_id": "stan@tri-starmedical.com-1"
+ },
+ "is_test": false,
+ "payment_complete_date": "2023-01-18T15:45:49Z",
+ "payment_status": "complete",
+ "payment_status_id": "PCA",
+ "purchase_date": "2023-01-18T15:45:48Z",
+ "purchase_id": 842819563,
+ "purchase_item": [
+ {
+ "accounting": {
+ "currency": "USD",
+ "product_net": 50.0,
+ "discount": 0.0,
+ "product_vat": 4.63,
+ "shipping": 0.0,
+ "shipping_vat": 0.0,
+ "eu_vat": -4.63,
+ "margin_net": -5.14,
+ "your_revenue": 44.86
+ },
+ "additional_information": [],
+ "currency": "USD",
+ "delivery_type": "Electronically",
+ "discount": 0.0,
+ "extended_download_price": 0.0,
+ "manual_order_price": 0.0,
+ "notification_no": 7945,
+ "product_id": 300525428,
+ "product_name": "AyaNova customization services",
+ "product_single_price": 50.0,
+ "purchase_item_key": [],
+ "quantity": 1,
+ "running_no": 1,
+ "shipping_price": 0.0,
+ "shipping_vat_pct": 0.0,
+ "vat_pct": 9.25
+ }
+ ],
+ "purchase_origin": "online",
+ "sequential_invoice_no": "e5-US-2023-00000083100"
+}
+}
+}
+
+
+/////////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////// TEST PURCHASE USING SPECIAL TEST CREDIT CARD NUMBER PROVIDED BY MYCOMMERCE
+{
+"creation_date": "2023-01-18T04:24:28Z",
+"id": 357424814,
+"order_notification": {
+"purchase": {
+ "customer_data": {
+ "billing_contact": {
+ "address": {
+ "city": "Courtenay",
+ "country": "Canada",
+ "country_id": "CA",
+ "postal_code": "V9J9T6",
+ "state": "British Columbia",
+ "state_id": "BC",
+ "street1": "05-3610 Christie Parkway"
+ },
+ "company": "GZTestCo",
+ "email": "gzmailadmin@gmail.com",
+ "first_name": "Test",
+ "last_name": "Testerson"
+ },
+ "customer_payment_data": {
+ "currency": "USD",
+ "payment_method": "Other"
+ },
+ "delivery_contact": {
+ "address": {
+ "city": "Courtenay",
+ "country": "Canada",
+ "country_id": "CA",
+ "postal_code": "V9J9T6",
+ "state": "British Columbia",
+ "state_id": "BC",
+ "street1": "05-3610 Christie Parkway"
+ },
+ "company": "GZTestCo",
+ "email": "gzmailadmin@gmail.com",
+ "first_name": "Test",
+ "last_name": "Testerson"
+ },
+ "language": "English",
+ "language_iso": "en",
+ "reg_name": "GZTestCo",
+ "shopper_id": "65817245",
+ "subscribe_newsletter": false,
+ "user_id": "gzmailadmin@gmail.com-36"
+ },
+ "is_test": true,
+ "payment_complete_date": "2023-01-18T04:24:28Z",
+ "payment_status": "testpaymentarrived",
+ "payment_status_id": "TCA",
+ "purchase_date": "2023-01-18T04:24:28Z",
+ "purchase_id": 842769483,
+ "purchase_item": [
+ {
+ "additional_information": [],
+ "currency": "USD",
+ "delivery_type": "Electronically",
+ "discount": 0.0,
+ "extended_download_price": 0.0,
+ "manual_order_price": 0.0,
+ "notification_no": 0,
+ "product_id": 300525428,
+ "product_name": "AyaNova customization services",
+ "product_single_price": 50.0,
+ "purchase_item_key": [],
+ "quantity": 1,
+ "running_no": 1,
+ "shipping_price": 0.0,
+ "shipping_vat_pct": 0.0,
+ "vat_pct": 12.0
+ }
+ ],
+ "purchase_origin": "online"
+}
+}
+}
+
+
+/////////////////////////////////////////////////////////////////////////////////////////////////////
+//////////////////////////////// RAVEN PERPETUAL
+
+{
+"creation_date": "2023-01-14T06:17:59Z",
+"id": 357214426,
+"order_notification": {
+"purchase": {
+ "customer_data": {
+ "billing_contact": {
+ "address": {
+ "city": "Frankfort",
+ "country": "USA",
+ "country_id": "US",
+ "postal_code": "46041",
+ "state": "Indiana",
+ "state_id": "IN",
+ "street1": "47 N Jackson St."
+ },
+ "company": "ACCS",
+ "email": "internet@accs.net",
+ "first_name": "Marcus",
+ "last_name": "Hodges"
+ },
+ "customer_payment_data": {
+ "currency": "USD",
+ "payment_method": "Visa"
+ },
+ "delivery_contact": {
+ "address": {
+ "city": "Frankfort",
+ "country": "USA",
+ "country_id": "US",
+ "postal_code": "46041",
+ "state": "Indiana",
+ "state_id": "IN",
+ "street1": "47 N Jackson St."
+ },
+ "company": "ACCS",
+ "email": "internet@accs.net",
+ "first_name": "Marcus",
+ "last_name": "Hodges"
+ },
+ "language": "English",
+ "language_iso": "en",
+ "reg_name": "ACCS",
+ "shopper_id": "65783365",
+ "subscribe_newsletter": false,
+ "user_id": "internet@accs.net"
+ },
+ "is_test": false,
+ "payment_complete_date": "2023-01-14T06:17:59Z",
+ "payment_status": "complete",
+ "payment_status_id": "PCA",
+ "purchase_date": "2023-01-14T06:17:58Z",
+ "purchase_id": 841935113,
+ "purchase_item": [
+ {
+ "accounting": {
+ "currency": "USD",
+ "product_net": 72.0,
+ "discount": 0.0,
+ "product_vat": 5.04,
+ "shipping": 0.0,
+ "shipping_vat": 0.0,
+ "eu_vat": -5.04,
+ "margin_net": -6.03,
+ "your_revenue": 65.97
+ },
+ "additional_information": [
+ {
+ "additional_id": "AGREENOREFUNDS",
+ "additional_value": "YES"
+ },
+ {
+ "additional_id": "AGREEPAYMETHODVALIDCANCEL",
+ "additional_value": "YES"
+ },
+ {
+ "additional_id": "AGREEEXPIRESIFNOTPAID",
+ "additional_value": "YES"
+ },
+ {
+ "additional_id": "DATABASEID",
+ "additional_value": "7ktPA+Eaq+LM4vNMkQdbwBkUMDzzFYXmH+m21n3w5Rk="
+ }
+ ],
+ "currency": "USD",
+ "delivery_type": "Electronically",
+ "discount": 0.0,
+ "extended_download_price": 0.0,
+ "manual_order_price": 0.0,
+ "notification_no": 7944,
+ "product_id": 301028317,
+ "product_name": "AyaNova perpetual single user license includes one year maintenance plan ",
+ "product_single_price": 24.0,
+ "purchase_item_key": [],
+ "quantity": 3,
+ "running_no": 1,
+ "shipping_price": 0.0,
+ "shipping_vat_pct": 0.0,
+ "subscription": {
+ "expiration_date": "2024-01-14T06:17:59Z",
+ "id": "841935113-1",
+ "interval": "Yearly without end",
+ "renewal_discount_count": "",
+ "renewal_discount_start": "",
+ "renewal_type": "auto",
+ "retention_discount_count": "",
+ "retention_discount_percent": "",
+ "start_date": "2023-01-14T00:00:00",
+ "status": "ToProcess",
+ "status_id": "TOP"
+ },
+ "vat_pct": 7.0,
+ "your_product_id": "perpetual"
+ }
+ ],
+ "purchase_origin": "online",
+ "sequential_invoice_no": "e5-US-2023-00000064245"
+}
+}
+}
+
+
+/////////////////////////////////////////////////////////////////////////////////////////////////////
+///////////////////////////////// RAVEN MONTHLY SUBSCRIPTION
+{
+"creation_date": "2023-01-07T01:54:11Z",
+"id": 356798113,
+"order_notification": {
+"purchase": {
+ "customer_data": {
+ "billing_contact": {
+ "address": {
+ "city": "UNIONVILLE",
+ "country": "Canada",
+ "country_id": "CA",
+ "postal_code": "L3R 9W6",
+ "state": "Ontario",
+ "state_id": "ON",
+ "street1": "4261 HWY 7 E, Unit A14 #399"
+ },
+ "company": "sportseffect",
+ "email": "service@sportseffect.com",
+ "first_name": "Mark",
+ "last_name": "Hopkins"
+ },
+ "customer_payment_data": {
+ "currency": "CAD",
+ "payment_method": "Visa"
+ },
+ "delivery_contact": {
+ "address": {
+ "city": "UNIONVILLE",
+ "country": "Canada",
+ "country_id": "CA",
+ "postal_code": "L3R 9W6",
+ "state": "Ontario",
+ "state_id": "ON",
+ "street1": "4261 HWY 7 E, Unit A14 #399"
+ },
+ "company": "sportseffect",
+ "email": "service@sportseffect.com",
+ "first_name": "Mark",
+ "last_name": "Hopkins"
+ },
+ "language": "English",
+ "language_iso": "en",
+ "reg_name": "sportseffect",
+ "shopper_id": "65387076",
+ "subscribe_newsletter": false,
+ "user_id": "service@sportseffect.com"
+ },
+ "is_test": false,
+ "payment_complete_date": "2023-01-07T01:54:11Z",
+ "payment_status": "complete",
+ "payment_status_id": "PCA",
+ "purchase_date": "2023-01-07T01:54:09Z",
+ "purchase_id": 840549123,
+ "purchase_item": [
+ {
+ "accounting": {
+ "currency": "USD",
+ "product_net": 23.8,
+ "discount": 0.0,
+ "product_vat": 3.09,
+ "shipping": 0.0,
+ "shipping_vat": 0.0,
+ "eu_vat": -3.09,
+ "margin_net": -4.03,
+ "your_revenue": 19.77
+ },
+ "additional_information": [
+ {
+ "additional_id": "AGREENOREFUNDS",
+ "additional_value": "YES"
+ },
+ {
+ "additional_id": "AGREEPAYMETHODVALIDCANCEL",
+ "additional_value": "YES"
+ },
+ {
+ "additional_id": "AGREEEXPIRESIFNOTPAID",
+ "additional_value": "YES"
+ },
+ {
+ "additional_id": "DATABASEID",
+ "additional_value": "New subscription"
+ }
+ ],
+ "currency": "USD",
+ "delivery_type": "Electronically",
+ "discount": 0.0,
+ "extended_download_price": 0.0,
+ "manual_order_price": 0.0,
+ "notification_no": 7937,
+ "product_id": 301028467,
+ "product_name": "AyaNova subscription one user monthly",
+ "product_single_price": 11.9,
+ "purchase_item_key": [],
+ "quantity": 2,
+ "running_no": 1,
+ "shipping_price": 0.0,
+ "shipping_vat_pct": 0.0,
+ "subscription": {
+ "expiration_date": "2023-02-07T00:15:13Z",
+ "id": "833674953-1",
+ "interval": "Monthly without end",
+ "original_notification_no": "7925",
+ "original_purchase_id": "833674953",
+ "original_running_no": "1",
+ "renewal_discount_count": "",
+ "renewal_discount_start": "",
+ "renewal_type": "auto",
+ "retention_discount_count": "",
+ "retention_discount_percent": "",
+ "start_date": "2022-12-07T00:00:00",
+ "status": "ToProcess",
+ "status_id": "TOP"
+ },
+ "vat_pct": 13.0,
+ "your_product_id": "subscriptionmonthly"
+ }
+ ],
+ "purchase_origin": "Subscription",
+ "sequential_invoice_no": "e5-DE-2023-00000055949"
+}
+}
+}
+
+ */
+
+ #endregion sample vendor notifications
+ //order notification json schema on this page down
+ //actual schema: https://api.shareit.com/xml/2.4/ordernotification.xsd
+ //https://account.mycommerce.com/home/wiki/7479997 //json format
+ //https://account.mycommerce.com/home/wiki/7479805 //overall info
+
+
+
+
+
+
+ /////////////////////////////////////////////////////////////////////
+
+ }//eoc
+
+
+}//eons
+
diff --git a/server/generator/SockBotProcessVendorNotifications.cs b/server/generator/SockBotProcessVendorNotifications.cs
index d1f633d..1e922fa 100644
--- a/server/generator/SockBotProcessVendorNotifications.cs
+++ b/server/generator/SockBotProcessVendorNotifications.cs
@@ -109,10 +109,31 @@ namespace Sockeye.Biz
//this is not the expected format data, stop processing and alert:
throw new System.FormatException($"Vendor data unexpected format:{vn.VendorData}");
}
+ //parse purchases collection needed up front as it potentially contains Customer record relevant data
+ var jaPurchaseList = (JArray)jData["order_notification"]["purchase"]["purchase_item"];
+ string RavenDBId = string.Empty;
+ foreach (JObject jPurchase in jaPurchaseList)
+ {
+ /////DATABASE ID if available used to matchup
+ //Capture raven database id if present
+ if (jPurchase["additional_information"] != null)
+ {
+ var jaAdditionalItems = (JArray)jPurchase["additional_information"];
+ foreach (JObject jAdditionalItem in jaAdditionalItems)
+ {
+ if (jAdditionalItem["additional_id"] != null && jAdditionalItem["additional_id"].Value() == "DATABASEID")
+ {
+ RavenDBId = jAdditionalItem["additional_value"].Value();
+ break;
+ }
+ }
+ }
+ }
///////////////////////////////////////////////////////////////////////////////////////////
#region CUSTOMER MAKE OR LOCATED
- ///////////////////////////////////////////////////////////////////////////////////////////
+ ///////////////////////////////////////////////////////////////////////////////////////////
+ //Note: always use reg name not customer name as it may vary between the two and regname is the one we care about
var jCustomerName = jData["order_notification"]["purchase"]["customer_data"]["reg_name"].Value() ?? throw new System.FormatException($"Vendor data empty reg_name:{vn.VendorData}");
if (jData["order_notification"]["purchase"]["customer_data"]["delivery_contact"]["email"] == null)//we can't process orders with no email at all hard no
throw new System.FormatException($"Vendor data empty email:{vn.VendorData}");
@@ -121,11 +142,22 @@ namespace Sockeye.Biz
var customerBiz = CustomerBiz.GetBiz(ct);
+
//attempt to match to existing customer
- //account number is most ideal match, name second but could be multiple in sockeye from rockfish sites so name will start the same, finally email if nothing else
- Customer customer = await ct.Customer.AsNoTracking().FirstOrDefaultAsync(z => z.AccountNumber == jCustomerAccountNumber)
- ?? await ct.Customer.AsNoTracking().FirstOrDefaultAsync(z => z.Name.StartsWith(jCustomerName))
- ?? await ct.Customer.AsNoTracking().FirstOrDefaultAsync(z => z.EmailAddress == jCustomerEmail);
+ //databaseID for raven is best match,account number is next most ideal match, , then name but could be multiple in sockeye from rockfish sites so name will start the same, finally email if nothing else
+ Customer customer = null;
+
+ //First best match is RavenDBID
+ if (!string.IsNullOrWhiteSpace(RavenDBId))
+ customer = await ct.Customer.AsNoTracking().FirstOrDefaultAsync(z => z.DbId == RavenDBId);
+
+ //if not found then try match in order of best matching
+ if (customer == null)
+ customer = await ct.Customer.AsNoTracking().FirstOrDefaultAsync(z => z.AccountNumber == jCustomerAccountNumber)
+ ?? await ct.Customer.AsNoTracking().FirstOrDefaultAsync(z => z.Name.StartsWith(jCustomerName))
+ ?? await ct.Customer.AsNoTracking().FirstOrDefaultAsync(z => z.EmailAddress == jCustomerEmail);
+
+ //still no match, consider it a new customer
if (customer == null)
{
//New customer
@@ -180,9 +212,7 @@ namespace Sockeye.Biz
if (await ct.Purchase.AnyAsync(z => z.SalesOrderNumber == salesOrderNumber))
throw new System.ApplicationException($"Sales order already exists: {salesOrderNumber} will not be processed");
- //iterate purchase items array
- var jaPurchaseList = (JArray)jData["order_notification"]["purchase"]["purchase_item"];
-
+ //iterate purchase items array
foreach (JObject jPurchase in jaPurchaseList)
{
Purchase p = new Purchase();
@@ -203,19 +233,8 @@ namespace Sockeye.Biz
p.Revenue = jPurchase["accounting"]["your_revenue"].Value();
}
- //Capture raven database id if present
- if (jPurchase["additional_information"] != null)
- {
- var jaAdditionalItems = (JArray)jPurchase["additional_information"];
- foreach (JObject jAdditionalItem in jaAdditionalItems)
- {
- if (jAdditionalItem["additional_id"] != null && jAdditionalItem["additional_id"].Value() == "DATABASEID")
- {
- p.DbId = jAdditionalItem["additional_value"].Value();
- break;
- }
- }
- }
+ if (!string.IsNullOrWhiteSpace(RavenDBId))
+ p.DbId = RavenDBId;
if (jPurchase["promotion_coupon"] != null)
p.CouponCode = jPurchase["promotion_coupon"].Value();
diff --git a/todo.txt b/todo.txt
index c1be718..b625807 100644
--- a/todo.txt
+++ b/todo.txt
@@ -1,7 +1,9 @@
TODO:
+Licenses JOB
+
+- VendorNotifications, add match by dbid to customer matching code
-Then on to licenses job
then on to all the basics, fetch licenses for v7 and v8, trial request processing trigger route, trial subscription server processing and trigger route
- JOB: Process purchases that are from vendor notification