From 98a405501b9caf4686dd5e8784f390acae7d1656 Mon Sep 17 00:00:00 2001 From: John Cardinal Date: Thu, 12 Jan 2023 19:19:27 +0000 Subject: [PATCH] --- server/Controllers/EnumListController.cs | 21 ++++++ server/DataList/ProductDataList.cs | 14 +--- server/DataList/SubscriptionServerDataList.cs | 11 +-- server/biz/ServerState.cs | 15 ++++ server/models/SubscriptionServer.cs | 3 +- server/util/AySchema.cs | 38 +++++++++- todo.txt | 71 +++++++++++++++++-- 7 files changed, 147 insertions(+), 26 deletions(-) create mode 100644 server/biz/ServerState.cs diff --git a/server/Controllers/EnumListController.cs b/server/Controllers/EnumListController.cs index e5af8ad..008c5a3 100644 --- a/server/Controllers/EnumListController.cs +++ b/server/Controllers/EnumListController.cs @@ -90,6 +90,7 @@ namespace Sockeye.Api.Controllers ret.Add(new KeyValuePair(StringUtil.TrimTypeName(typeof(SockDaysOfWeek).ToString()), "Days of the week")); ret.Add(new KeyValuePair(StringUtil.TrimTypeName(typeof(TrialRequestStatus).ToString()), "Trial license request status")); ret.Add(new KeyValuePair(StringUtil.TrimTypeName(typeof(ProductGroup).ToString()), "Product group")); + ret.Add(new KeyValuePair(StringUtil.TrimTypeName(typeof(ServerState).ToString()), "Server state")); return Ok(ApiOkResponse.Response(ret)); } @@ -587,6 +588,26 @@ namespace Sockeye.Api.Controllers ReturnList.Add(new NameIdItem() { Name = LT["ProductGroupRavenPerpetual"], Id = (long)ProductGroup.RavenPerpetual }); ReturnList.Add(new NameIdItem() { Name = LT["ProductGroupRavenSubscription"], Id = (long)ProductGroup.RavenSubscription }); } + else if (keyNameInLowerCase == StringUtil.TrimTypeName(typeof(ServerState).ToString()).ToLowerInvariant()) + { + TranslationKeysToFetch.Add("ServerStateRequested"); + TranslationKeysToFetch.Add("ServerStateActiveHealthy"); + TranslationKeysToFetch.Add("ServerStateActiveRequiresAttention"); + TranslationKeysToFetch.Add("ServerStateFailFirstHealthCheck"); + TranslationKeysToFetch.Add("ServerStateFailSecondHealthCheck"); + TranslationKeysToFetch.Add("ServerStateFailedRequiresAttention"); + TranslationKeysToFetch.Add("ServerStateDeActivated"); + TranslationKeysToFetch.Add("ServerStateDestroyed"); + var LT = await TranslationBiz.GetSubsetStaticAsync(TranslationKeysToFetch, translationId); + ReturnList.Add(new NameIdItem() { Name = LT["ServerStateRequested"], Id = (long)ServerState.Requested }); + ReturnList.Add(new NameIdItem() { Name = LT["ServerStateActiveHealthy"], Id = (long)ServerState.ActiveHealthy }); + ReturnList.Add(new NameIdItem() { Name = LT["ServerStateActiveRequiresAttention"], Id = (long)ServerState.ActiveRequiresAttention }); + ReturnList.Add(new NameIdItem() { Name = LT["ServerStateFailFirstHealthCheck"], Id = (long)ServerState.FailFirstHealthCheck }); + ReturnList.Add(new NameIdItem() { Name = LT["ServerStateFailSecondHealthCheck"], Id = (long)ServerState.FailSecondHealthCheck }); + ReturnList.Add(new NameIdItem() { Name = LT["ServerStateFailedRequiresAttention"], Id = (long)ServerState.FailedRequiresAttention }); + ReturnList.Add(new NameIdItem() { Name = LT["ServerStateDeActivated"], Id = (long)ServerState.DeActivated }); + ReturnList.Add(new NameIdItem() { Name = LT["ServerStateDestroyed"], Id = (long)ServerState.Destroyed }); + } diff --git a/server/DataList/ProductDataList.cs b/server/DataList/ProductDataList.cs index a6a3bb9..00bf213 100644 --- a/server/DataList/ProductDataList.cs +++ b/server/DataList/ProductDataList.cs @@ -17,19 +17,7 @@ namespace Sockeye.DataList DefaultColumns = new List() { "ProductName", "ProductGroup", "ProductVendorCode", "ProductOurCode" }; DefaultSortBy = new Dictionary() { { "ProductName", "+" } }; FieldDefinitions = new List(); - /* -await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'Product', 'Product' FROM atranslation t where t.baselanguage = 'en'"); -await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'ProductList', 'Products' FROM atranslation t where t.baselanguage = 'en'"); -await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'ProductName', 'Name' FROM atranslation t where t.baselanguage = 'en'"); -await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'ProductLicenseInterval', 'License period' FROM atranslation t where t.baselanguage = 'en'"); -await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'ProductMaintInterval', 'Maintenance period' FROM atranslation t where t.baselanguage = 'en'"); -await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'ProductVendorCode', 'Vendor code' FROM atranslation t where t.baselanguage = 'en'"); -await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'ProductOurCode', 'Our code' FROM atranslation t where t.baselanguage = 'en'"); -id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name TEXT NOT NULL, active BOOL NOT NULL DEFAULT true, " -+ "vendorid BIGINT NOT NULL REFERENCES avendor(id), licenseinterval INTERVAL, maintinterval INTERVAL, vendorcode TEXT NOT NULL, ourcode TEXT NOT NULL, " -+ "wiki TEXT, tags VARCHAR(255) ARRAY - */ - + FieldDefinitions.Add(new DataListFieldDefinition { TKey = "ProductName", diff --git a/server/DataList/SubscriptionServerDataList.cs b/server/DataList/SubscriptionServerDataList.cs index 78283bf..70fdfe4 100644 --- a/server/DataList/SubscriptionServerDataList.cs +++ b/server/DataList/SubscriptionServerDataList.cs @@ -14,7 +14,7 @@ namespace Sockeye.DataList var RoleSet = BizRoles.GetRoleSet(DefaultListAType); AllowedRoles = RoleSet.ReadFullRecord | RoleSet.Change; - DefaultColumns = new List() { "SubServerSubExpire", "SubServerName", "Customer", "SubServerTrial", "SubServerDatacenter", "Active" }; + DefaultColumns = new List() { "SubServerSubExpire", "SubServerName", "Customer", "SubServerTrial", "SubServerDatacenter", "ServerState" }; DefaultSortBy = new Dictionary() { { "SubServerSubExpire", "+" } }; FieldDefinitions = new List(); @@ -31,10 +31,11 @@ namespace Sockeye.DataList FieldDefinitions.Add(new DataListFieldDefinition { - TKey = "Active", - FieldKey = "Active", - UiFieldDataType = (int)UiFieldDataType.Bool, - SqlValueColumnName = "asubscriptionserver.active" + TKey = "ServerState", + FieldKey = "ServerState", + UiFieldDataType = (int)UiFieldDataType.Enum, + EnumType = Sockeye.Util.StringUtil.TrimTypeName(typeof(ServerState).ToString()), + SqlValueColumnName = "asubscriptionserver.serverstate" }); diff --git a/server/biz/ServerState.cs b/server/biz/ServerState.cs new file mode 100644 index 0000000..bf4e028 --- /dev/null +++ b/server/biz/ServerState.cs @@ -0,0 +1,15 @@ +namespace Sockeye.Biz +{ + public enum ServerState + { + //Any of these changes trigger event log and potentially notification event + Requested = 0, //New server requested, not physically present yet but needs to be created and activated + ActiveHealthy = 1, //running normally no actions required + ActiveRequiresAttention=2,//running but something is up, maybe updates required to linux or it's responding slowly or low on disk space, event triggered, event log entry etc + FailFirstHealthCheck = 3, //first check failed + FailSecondHealthCheck = 4, //second check failed + FailedRequiresAttention = 5, //failed 3 health checks needs someone to physically intervene, triggers notify event + DeActivated = 6, //swtiched off, unavailable, about to be destroyed usually when customer stops paying given grace period + Destroyed = 7// historical, not an active server doesn't need to be tracked or dealt with + } +}//eons \ No newline at end of file diff --git a/server/models/SubscriptionServer.cs b/server/models/SubscriptionServer.cs index 811ffa9..2001065 100644 --- a/server/models/SubscriptionServer.cs +++ b/server/models/SubscriptionServer.cs @@ -15,7 +15,8 @@ namespace Sockeye.Models [Required] public string Name { get; set; } public string IPAddress { get; set; } - public bool Active { get; set; } + [Required] + public ServerState ServerState { get; set; } = ServerState.Requested; public string Notes { get; set; } public DateTime Created { get; set; } [Required] diff --git a/server/util/AySchema.cs b/server/util/AySchema.cs index eb1f4d4..cc18474 100644 --- a/server/util/AySchema.cs +++ b/server/util/AySchema.cs @@ -886,7 +886,7 @@ $BODY$ LANGUAGE PLPGSQL STABLE"); + "pgroup INTEGER NOT NULL DEFAULT 0, tags VARCHAR(255) ARRAY )"); - await ExecQueryAsync("CREATE TABLE asubscriptionserver (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, active BOOL NOT NULL DEFAULT true, created TIMESTAMPTZ NOT NULL, " + await ExecQueryAsync("CREATE TABLE asubscriptionserver (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, serverstate INTEGER NOT NULL DEFAULT 0, created TIMESTAMPTZ NOT NULL, " + "name TEXT NOT NULL, ipaddress TEXT, notes TEXT, datacenter TEXT NOT NULL, timezone TEXT NOT NULL, dbid TEXT, lastupdated TIMESTAMPTZ, subscriptionexpire TIMESTAMPTZ NOT NULL, " + "trial BOOL NOT NULL DEFAULT true, trialcontact TEXT, trialemail TEXT, trialcompany TEXT, operatingsystem TEXT, customersubdomain TEXT, cost DECIMAL(38,18) NOT NULL default 7, " + "wiki TEXT, tags VARCHAR(255) ARRAY, customerid BIGINT REFERENCES acustomer(id) )"); @@ -1046,6 +1046,15 @@ $BODY$ LANGUAGE PLPGSQL STABLE"); await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'SubServerTrialCompany', 'Trial company' FROM atranslation t where t.baselanguage = 'en'"); await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'SubServerOperatingSystem', 'OS' FROM atranslation t where t.baselanguage = 'en'"); await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'SubServerCustomerDomain', 'Customer subdomain' FROM atranslation t where t.baselanguage = 'en'"); + await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'ServerState', 'Server state' FROM atranslation t where t.baselanguage = 'en'"); + await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'ServerStateRequested', 'Requested' FROM atranslation t where t.baselanguage = 'en'"); + await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'ServerStateActiveHealthy', 'Active healthy' FROM atranslation t where t.baselanguage = 'en'"); + await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'ServerStateActiveRequiresAttention', 'Active requires attention' FROM atranslation t where t.baselanguage = 'en'"); + await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'ServerStateFailFirstHealthCheck', 'Fail first health check' FROM atranslation t where t.baselanguage = 'en'"); + await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'ServerStateFailSecondHealthCheck', 'Fail second health check' FROM atranslation t where t.baselanguage = 'en'"); + await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'ServerStateFailedRequiresAttention', 'Failed requires attention' FROM atranslation t where t.baselanguage = 'en'"); + await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'ServerStateDeActivated', 'Deactivated' FROM atranslation t where t.baselanguage = 'en'"); + await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'ServerStateDestroyed', 'Destroyed' FROM atranslation t where t.baselanguage = 'en'"); //spanish translations await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'SubscriptionServer', 'Subscription server' FROM atranslation t where t.baselanguage = 'es'"); @@ -1063,6 +1072,15 @@ $BODY$ LANGUAGE PLPGSQL STABLE"); await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'SubServerTrialCompany', 'Trial company' FROM atranslation t where t.baselanguage = 'es'"); await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'SubServerOperatingSystem', 'OS' FROM atranslation t where t.baselanguage = 'es'"); await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'SubServerCustomerDomain', 'Customer subdomain' FROM atranslation t where t.baselanguage = 'es'"); + await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'ServerState', 'Server state' FROM atranslation t where t.baselanguage = 'es'"); + await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'ServerStateRequested', 'Requested' FROM atranslation t where t.baselanguage = 'es'"); + await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'ServerStateActiveHealthy', 'Active healthy' FROM atranslation t where t.baselanguage = 'es'"); + await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'ServerStateActiveRequiresAttention', 'Active requires attention' FROM atranslation t where t.baselanguage = 'es'"); + await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'ServerStateFailFirstHealthCheck', 'Fail first health check' FROM atranslation t where t.baselanguage = 'es'"); + await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'ServerStateFailSecondHealthCheck', 'Fail second health check' FROM atranslation t where t.baselanguage = 'es'"); + await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'ServerStateFailedRequiresAttention', 'Failed requires attention' FROM atranslation t where t.baselanguage = 'es'"); + await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'ServerStateDeActivated', 'Deactivated' FROM atranslation t where t.baselanguage = 'es'"); + await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'ServerStateDestroyed', 'Destroyed' FROM atranslation t where t.baselanguage = 'es'"); //french translations await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'SubscriptionServer', 'Subscription server' FROM atranslation t where t.baselanguage = 'fr'"); @@ -1080,6 +1098,15 @@ $BODY$ LANGUAGE PLPGSQL STABLE"); await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'SubServerTrialCompany', 'Trial company' FROM atranslation t where t.baselanguage = 'fr'"); await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'SubServerOperatingSystem', 'OS' FROM atranslation t where t.baselanguage = 'fr'"); await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'SubServerCustomerDomain', 'Customer subdomain' FROM atranslation t where t.baselanguage = 'fr'"); + await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'ServerState', 'Server state' FROM atranslation t where t.baselanguage = 'fr'"); + await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'ServerStateRequested', 'Requested' FROM atranslation t where t.baselanguage = 'fr'"); + await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'ServerStateActiveHealthy', 'Active healthy' FROM atranslation t where t.baselanguage = 'fr'"); + await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'ServerStateActiveRequiresAttention', 'Active requires attention' FROM atranslation t where t.baselanguage = 'fr'"); + await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'ServerStateFailFirstHealthCheck', 'Fail first health check' FROM atranslation t where t.baselanguage = 'fr'"); + await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'ServerStateFailSecondHealthCheck', 'Fail second health check' FROM atranslation t where t.baselanguage = 'fr'"); + await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'ServerStateFailedRequiresAttention', 'Failed requires attention' FROM atranslation t where t.baselanguage = 'fr'"); + await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'ServerStateDeActivated', 'Deactivated' FROM atranslation t where t.baselanguage = 'fr'"); + await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'ServerStateDestroyed', 'Destroyed' FROM atranslation t where t.baselanguage = 'fr'"); //german translations await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'SubscriptionServer', 'Subscription server' FROM atranslation t where t.baselanguage = 'de'"); @@ -1097,6 +1124,15 @@ $BODY$ LANGUAGE PLPGSQL STABLE"); await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'SubServerTrialCompany', 'Trial company' FROM atranslation t where t.baselanguage = 'de'"); await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'SubServerOperatingSystem', 'OS' FROM atranslation t where t.baselanguage = 'de'"); await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'SubServerCustomerDomain', 'Customer subdomain' FROM atranslation t where t.baselanguage = 'de'"); + await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'ServerState', 'Server state' FROM atranslation t where t.baselanguage = 'de'"); + await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'ServerStateRequested', 'Requested' FROM atranslation t where t.baselanguage = 'de'"); + await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'ServerStateActiveHealthy', 'Active healthy' FROM atranslation t where t.baselanguage = 'de'"); + await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'ServerStateActiveRequiresAttention', 'Active requires attention' FROM atranslation t where t.baselanguage = 'de'"); + await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'ServerStateFailFirstHealthCheck', 'Fail first health check' FROM atranslation t where t.baselanguage = 'de'"); + await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'ServerStateFailSecondHealthCheck', 'Fail second health check' FROM atranslation t where t.baselanguage = 'de'"); + await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'ServerStateFailedRequiresAttention', 'Failed requires attention' FROM atranslation t where t.baselanguage = 'de'"); + await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'ServerStateDeActivated', 'Deactivated' FROM atranslation t where t.baselanguage = 'de'"); + await ExecQueryAsync("INSERT INTO atranslationitem(translationid,key,display) SELECT t.id, 'ServerStateDestroyed', 'Destroyed' FROM atranslation t where t.baselanguage = 'de'"); #endregion subscriptionserver diff --git a/todo.txt b/todo.txt index 54b3f2b..923a575 100644 --- a/todo.txt +++ b/todo.txt @@ -1,13 +1,65 @@ +TODO: + +- Subscriptionserverbiz state change trigger potential notification and event log event!!! + +- Event log and event for server state change event should trigger an entry in the event log automatically +- events serverstate change can subscribe and be alerted etc + +- Purchase event must trigger notification event subscribable (might already due to created etc) + +- Message templates back and front + new global setting + +- new notification event type and related code back and front for subscriptions etc + want all old rockfish style and new sockeye notifications to go through proper notify event code + - new: trial subscription server requested + - new: trial subscription server expiration + - new: subscription server expiration past + so if they don't re-up no active license then it warns me to shut it down + One for shutting down server and another for decommissioning + + +- v7 license fetch route +- v8 license fetch route +- v8 trial request route +- shareit payment notification route + triggers purchase if of that type, needs to analyze it and need to test it out somehow here first +- trial server request route that contact form can trigger +- JOB: notify user active license +- JOB: purchase to license +- JOB: Ping / check health route of subscription server + flag last health check + trigger event notification if fails + serverstate set so that it maybe has an OneFail then a TwoFail then a FAILED state where it notifies me so I don't get transient alerts + also server state used for other things like pending but not commissioned yet, decommissioned etc +- notify me trial request + +- manually simulate v7 fetch from rockfish compare to manual test locally in sockey confirm + Same data shape and format + fetched is set correctly + headers + tls version (once online are the same, should be since it's going through nginx technically for the tls part) +- Test v8 key fetch, trial request, the whole shebang locally +- ONLINE TEST + Install and put on sockeye.ayanova.com subdomain in nginx on alternate port + Test ui functionality logins etc + + +- SUBSCRIPTION SERVER UI + - Menu option generate server commision script based on settings in form + can fill out form, click on it and it will generate the script needed to paste into new server + - Button to trigger D.O. API to requisition server automatically, spin it up etc + + + + + +//////////////////////////////////////////////////////////////////////////////////////////// +//OLD LICENSE NOTES -Move license from trialrequest to license object and set the linking id of the license on the request -Import fixup, list fixup, ui fixup -Basically, want only one place for a license to exist and that's in license table -trial requests just drive the process, dont' actually cotnain a license anymore - -pgsql.PostgresException : 42703: column "keyid" of relation "atriallicenserequest" does not exist ------ @@ -32,6 +84,13 @@ ALSO Needs manual license generation for v7 still + +** CONTACT FORM +Server request for trial subscription server should maybe go through sockeye instead as a form people can request from or the contact app should forward to sockeye +so it can create a new subscription server record that is pending status for me to just approve and ultimately auto-generate a server using D.O. API or whatever + + + ========= PURCHASE drives new licensing ui