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