This project is the customer portal automatically generated by the Umbraco CMS
Please observe the branch structure for this repo.
master - this is the top level branch which can be cloned and downloaded by any authorised user, but only the Total Coding administrator can push changes to it. This represents the most stable software version. After the first release, it will represent the version currently published to the production environment.
release1 - this is the current release branch and therefore the default for the repo. All collaborating developers are required to push changes to their own branch first and then generate a pull request to merge it with release1. Upon induction to the project, each developer will be assigned their own branch for individual work.
Each developer has his/hers own branch. Please commit all your code to that branch, and once ready to merge into release, create a Pull Requet. Your code will be reviewed and merged into the release1 branch after review.
Once completed with a milestone (release or sprint), create a new Tag from the tip of the current branch (developer's branch). Create the new Tag using Visual Studio, with a simple name like "Phase-1-Milestone-1" and put the details of the Milestone in the Description. Then push the new Tag to the repository - That will be the "package" of the completed milestone.
The purpose of this project is to create a multi-tenant system that will host several sites. Each site will be completely separate from each other, with it's own preferences and requirements. There will be multiple possible templates (html) options available in the near future, for tenants to choose from, but initially there is only one design for all tenants.
Umbraco has been adapted to provide a Json-Rpc interface for the Total Code System, exposing a large set of Api procedures allowing the external system to control the creation of tenant websites. Each tenant website has its own site structure, registration, login, and so on.
The tenant sites are multi-language, and provide a front-end interface to all procedures that are processed via Api in the Total Code System. The "Create Tenant" procedure, once called by the Total Code System, creates a new website (content node root with all it's descendants) and sets the basic details and preferences for the tenant. The Umbraco CMS is not a static site, but a constantly changing Tenant Website Collection.
http://customer-management-service-api.totalcoding-test1.com/swagger/index.html
- The back-end is .NET Core 2.2
- Umbraco version 8.0.2 is .NET Framework 4.7.2
- Microsoft Visual Studio 2017
- Microsoft Sql Server Express 2014 or above
- IIS Express (which comes with Visual Studio)
There are several other depedencies which are already configured in the 'packages.config' file. These dependencies are auto-restored once the solution is built (Ctrl+Shift+B)
- Restore the Database file (included in the Database folder for the solution) [either umbraco.cms.8.0.2-publish.bak or umbraco.cms.8.0.2-publish.bacpac]. Delete the old DB and restore fresh, whenever needed.
Change the Connection String for the SQL server, in the web.config, line 49
<connectionStrings>
<remove name="umbracoDbDSN"/>
<add name="umbracoDbDSN" connectionString="Server=.\sqlexpress2014;Database=umbraco.cms.8.0.2;Integrated Security=true" providerName="System.Data.SqlClient"/>
</connectionStrings>
Change where it reads .\sqlexpress2014 to whatever server you need.
Then you can run (F5)
- url: http://localhost:2766/umbraco
- username: [email protected]
- password: >41*_Vkieb
Use Postman to test the Rpc endpoints (see exported collections)
Follow the documentation found in each Procedure
The Json-Rpc controller processes all external Api calls and manipulates the Umbraco system to create, edit, change password, purge tenants, and etc.
The Json-Rpc Controller references the ControllerService / ControllerHelper class to process the actual work. (previously the controller was located in the Helpers folder, as ControllerHelper).
The Document Types are programmatically created, checking for their existance in the Umbraco CMS and creating themselves upon first run.
Example: The Home Document Type is 'auto created' with an IComponent Class:
refer to the links below for additional information
private void CreateHomeDocumentType()
{
try
{
// Check if container already exists
var container = contentTypeService.GetContainers(CONTAINER, 1).FirstOrDefault();
int containerId = 0;
if (container == null)
{
// if container doesn't exist, create the container (this is only needed for the Home Document Type, other document types don't need this check)
var newcontainer = contentTypeService.CreateContainer(-1, CONTAINER);
if (newcontainer.Success)
containerId = newcontainer.Result.Entity.Id;
}
else
{
containerId = container.Id;
}
// Check if the Document Type Already Exists and skip if it does
var contentType = contentTypeService.Get(DOCUMENT_TYPE_ALIAS);
if (contentType == null)
{
ContentType docType = (ContentType)contentType ?? new ContentType(containerId)
{
Name = DOCUMENT_TYPE_NAME,
Alias = DOCUMENT_TYPE_ALIAS,
AllowedAsRoot = true,
Description = DOCUMENT_TYPE_DESCRIPTION,
Icon = ICON,
SortOrder = 0,
Variations = ContentVariation.Culture
};
// Create the Template if it doesn't exist
if (fileService.GetTemplate(TEMPLATE_ALIAS) == null)
{
// then create the template
Template newTemplate = new Template(TEMPLATE_NAME, TEMPLATE_ALIAS);
fileService.SaveTemplate(newTemplate);
}
// Set templates for document type
var template = fileService.GetTemplate(TEMPLATE_ALIAS);
docType.AllowedTemplates = new List<ITemplate> { template };
docType.SetDefaultTemplate(template);
docType.AddPropertyGroup(CONTENT_TAB);
docType.AddPropertyGroup(TENANT_TAB);
// Set Document Type Properties
#region Tenant Home Page Content
PropertyType brandLogoPropType = new PropertyType(dataTypeService.GetDataType(1043), "brandLogo")
{
Name = "Brand Logo",
Variations = ContentVariation.Nothing
};
docType.AddPropertyType(brandLogoPropType, CONTENT_TAB);
// more properties
#endregion
#region Tenant Info Tab
PropertyType tenantUidPropType = new PropertyType(dataTypeService.GetDataType(-92), "tenantUid")
{
Name = "Tenant Uid",
Description = "Tenant Unique Id",
Variations = ContentVariation.Nothing
};
docType.AddPropertyType(tenantUidPropType, TENANT_TAB);
// more properties
#endregion
contentTypeService.Save(docType); // Save the document type
ConnectorContext.AuditService.Add(AuditType.New, -1, docType.Id, "Document Type", $"Document Type {DOCUMENT_TYPE_NAME} has been created"); // Notify the Audit to keep track of changes
ContentHelper.CopyPhysicalAssets(new EmbeddedResources()); // Copy any physical resources (files) from the Assembly (saved as embedded resources)
}
}
catch (System.Exception ex) // Make sure to log any errors in the Umbraco Log
{
logger.Error(typeof(HomeDocumentType), ex.Message);
logger.Error(typeof(HomeDocumentType), ex.StackTrace);
}
}
Notice that the Document Type is created programmatically. All document types should be created programmatically, and not depend on any packages or manual work.
The content node is also created programmatically during the "Create Tenant" procedure and creates the home node.
HomeContentNode.cs
public int CreateHome(Tenant tenant)
{
var nodeName = tenant.Name;
var nodeAlias = tenant.Name.Trim(' ').ToLower();
var docType = contentTypeService.Get(HomeDocumentType.DOCUMENT_TYPE_ALIAS);
Validate(tenant); // this method verifies if the Tenant exists and if the data is correct
try
{
IContent tenantNode = contentService.Create(nodeName, -1, HomeDocumentType.DOCUMENT_TYPE_ALIAS);
tenantNode.SetCultureName(nodeName, tenant.Languages.Default); // since every site is multi language, setting the Culture name is required
// Alternate Languages
foreach (var language in tenant.Languages.Alternate)
{
tenantNode.SetCultureName($"{nodeName}-{language}", language);
}
// Set values for node
tenantNode.SetValue("brandName", tenant.BrandName);
tenantNode.SetValue("tenantUid", tenant.TenantUId);
tenantNode.SetValue("domain", tenant.Domain);
tenantNode.SetValue("subDomain", tenant.SubDomain);
tenantNode.SetValue("apiKey", tenant.ApiKey);
tenantNode.SetValue("appId", tenant.AppId);
tenantNode.SetValue("defaultLanguage", tenant.Languages.Default);
tenantNode.SetValue("tenantStatus", ENABLED);
tenantNode.SetValue("languages", string.Join(", ", tenant.Languages.Alternate.ToList()));
tenantNode.SetValue("tenantPreferencesProperty", JsonConvert.SerializeObject(tenant.TenantPreferences));
contentService.Save(tenantNode);
ConnectorContext.AuditService.Add(AuditType.Unpublish, -1, tenantNode.Id, "Content Node", $"ContentNode for {tenant.TenantUId} has been created");
return tenantNode.Id; // Notify Audit
}
catch (Exception ex)
{
logger.Error(typeof(HomeContentNode), ex.Message);
logger.Error(typeof(HomeContentNode), ex.StackTrace);
throw;
}
}
All Content Nodes for each tenant are created programmatically whenever the "Create Tenant" procedure is called.
The media node is also created programmatically during the "Create Tenant" procedure and creates the media home node.
HomeMediaNode.cs
public int CreateMediaHome(Tenant tenant)
{
Validate(tenant); // validates to check if media node already exists
try
{
// creates the node based off the tenant's brand name
var nodeName = tenant.Name;
var nodeAlias = tenant.Name.Trim(' ').ToLower();
var folder = mediaService.CreateMedia(nodeName, Constants.System.Root, "Folder");
mediaService.Save(folder);
ConnectorContext.AuditService.Add(AuditType.New, -1, folder.Id, "Media Folder", $"Media Home for {tenant.TenantUId} has been created"); // Notifies Audit
return folder.Id;
}
catch (System.Exception ex)
{
logger.Error(typeof(HomeMediaNode), ex.Message);
logger.Error(typeof(HomeMediaNode), ex.StackTrace);
throw;
}
}
All Media Nodes for each tenant are created programmatically whenever the "Create Tenant" procedure is called.
All repeatable text from pages should be registered as a Dictionary Item (Translate Section). Text such as buttons, inputs, labels, etc.
In your Razor code, make sure to insert a default value for each item, i.e.
@Umbraco.GetDictionaryValue("[ParentKey]Key", "Default Value")
Create new dictionary items by inheriting from the Umbraco.Plugins.Connector.Interfaces.IDictionaryItem interface
public class Register_RegisterTitle : IDictionaryItem
{
public string ParentKey => "Register Page";
public string Key => "[Register Page]Register Title";
public string Value => "Register";
public string LanguageCode => Countries.EnglishUnitedStates.GetCountryCode();
public Dictionary<string, string> Translations => new Dictionary<string, string>
{
{ Countries.EnglishUnitedKingdom.GetCountryCode(), "Register" },
{ Countries.SpanishSpainInternationalSort.GetCountryCode(), "Registrarse" }
};
}
Use the Umbraco.Plugins.Connector.Helpers.Country helper to ease the use of ISO Codes for countries.
After creating the Dictionary Classes, use the LanguageDictionaryService (formally LanguageDictionary) to create the items in Umbraco. (previously the LanguageDictionary was located in the Content Folder and was moved to Services)
// Add Dictionary Items
var dictionaryItems = new List<Type>
{
typeof(Register_RegisterRoot),
typeof(Register_RegisterTitle)
};
var language = new LanguageDictionaryService(ConnectorContext.LocalizationService, ConnectorContext.DomainService, ConnectorContext.Logger);
language.CreateDictionaryItems(dictionaryItems); // Create Dictionary Items
Dictionary Items will be created programmatically.
please refer to Dictionary folder not found in Umbraco 8 for details as it has changed from version 7.
Templates and physical files must be saved in the Plugin Solution as Embedded Resources, and must be copied to the Umbraco CMS system upon first run.
Each Document Type references an Embedded Resources class, that contains a list of all the resources to be copied.
For the Home (and initial setup), these files were copied
public class EmbeddedResources : IEmbeddedResource
{
public List<EmbeddedResource> Resources => new List<EmbeddedResource>
{
new EmbeddedResource{ FileName = "package.manifest", ResourceLocation = "Content.App_Plugins.ApiSettingsSurface.backoffice", OutputDirectory = "App_Plugins\\ApiSettingsSurface\\backoffice", ResourceType = ResourceType.Other },
new EmbeddedResource{ FileName = "*.*", ResourceLocation = "Content.App_Plugins.ApiSettingsSurface.backoffice.ApiSettings", OutputDirectory = "App_Plugins\\ApiSettingsSurface\\backoffice\\ApiSettings", ResourceType = ResourceType.Directory },
new EmbeddedResource{ FileName = "TotalCodeTenantHomeTemplate.cshtml", ResourceLocation = "Templates", OutputDirectory = "Views", ResourceType = ResourceType.NonTemplateView, Name = "Total Code Tenant Home", Alias = "TotalCodeTenantHome" },
new EmbeddedResource{ FileName = "Global.asax", ResourceLocation = "Content", OutputDirectory = ".\\", ResourceType = ResourceType.Other, Replace = true },
new EmbeddedResource{ FileName = "Global.asax.cs", ResourceLocation = "Content", OutputDirectory = ".\\", ResourceType = ResourceType.Other, AddToVisualStudioProject = true, DependentUpon = true, DependentUponFile = "Global.asax" }
};
}
It is possible to copy:
- Template
- NonTemplateView
- Layout
- Script
- Partial
- Macro
- Style
- Image
- Directory
All files should be added programmatically via Embedded Resources
- For technical questions about the code and Umbraco, contact Carlos Casalicchio
- For technical questions regarding deployment and repository, contact Paul Jones
- For project requirements, contact Dilek Isci
- For management questions, contact Nima Shakouri
In order to register, login, reset password, and so on, you will need to:
To register, you may use
password: P@ss0000
Tenant Uid = 50046935-C7CA-4AB9-B504-9000D5A52906
Tenant Uid = 020f8363-ca8c-4ee3-a3e2-624efbcb4453
App Id = GamingGuid
Api Key = api_key_Umbracotest27_Gaming
origin http://generic-domain2.com
origin http://customer-management-service-api.totalcoding-test1.com
origin http://umbraco28.totalbetting.xyz/
Phone +441500000000 (or 01, or 02, or 03, and so on)
- Open Postman - Follow the documentation in the Postman Post Calls - Update the Call according to your needs (check with Dilek for correct Tenant Data) - Once created, navigate to http://localhost:2766
- Open the browser console window and watch for server responses
- Watch for console responses to check for success or failures (see examples below)
MissingField = 112,
InvalidEmailFormat = 113,
InvalidDate = 114,
InvalidAge = 115,
MobileOrEmailRequired = 116,
InvalidCountry = 117,
InvalidCurrency = 118,
InvalidLanguage = 119,
InvalidTimeZone = 120,
ExistingCustomer = 121,
EmailNotFound = 122,
EmailSendFail = 123,
EmailAlreadyVerified = 124,
InvalidCustomer = 125,
InvalidOldPassword = 126,
MatchingOldAndNewPassword = 127,
InvalidCustomerStatus = 128,
CustomerNotFound = 129,
VerificationRecordNotFound = 130,
VerificationEmailExpired = 131,
ValidationCodeExpired = 133,
ValidationCodeSendFail = 134,
SMSSendFail = 135,
InvalidMobileNumber = 136,
InvalidVerificationEmail = 137,
VerificationCodeLimitExceeded = 138,
MobileNumberNotFound = 139,
{
"accessToken":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImFwaV9rZXlfY3VzdG9tZXJfbWFuYWdlbWVudCIsIm5hbWVpZCI6IkREMTE0MUJCLTM5NTctNENFMC05ODk5LTE5MUFCN0I2OEEyRiIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvdXNlcmRhdGEiOiJ7XCJUZW5hbnRHdWlkXCI6XCJcIixcIlBsYXRmb3JtR3VpZFwiOlwiQ3VzdG9tZXJNYW5hZ2VtZW50R3VpZFwiLFwiUGFyZW50QWNjb3VudElkXCI6bnVsbH0iLCJuYmYiOjE1NjMwMzA3ODMsImV4cCI6MTU2MzAzNDM4MywiaWF0IjoxNTYzMDMwNzgzLCJpc3MiOiJUb3RhbENvZGluZyJ9.5PsBAFimmgJwucMwdSte5KgAOlolj-tVDYF0gW-MVXc",
"refreshToken":"not-ready",
"expires":"2019-07-13T16:13:03.6387781Z"
}
{
"ApiKey":[
"ApiKey is missing"
],
"PlatformGuid":[
"PlatformGuid is missing"
]
}
{
"success":true,
"message":"Success! Verification code: 238140",
"errors":null
}
{
"success":false,
"message":"Fail",
"errors":{
"errorCode":136,
"errorMessage":"Invalid mobile number!"
}
}
{
"success":true,
"message":"Success! Verification code: 778674",
"errors":null
}
{
"success":false,
"message":"Fail",
"errors":{
"errorCode":136,
"errorMessage":"Invalid mobile number!"
}
}
{
"success":true,
"message":"Success!",
"errors":null
}
{
"success":false,
"message":"Fail",
"errors":{
"errorCode":140,
"errorMessage":"Validation code invalid!"
}
}
{
"success":false,
"message":"Fail",
"errors":{
"errorCode":138,
"errorMessage":"Limit exceeded for verification code requests!"
}
}
{
"success":true,
"message":"Success",
"payload":{
"id":159,
"title":"",
"firstName":"sadfa",
"lastName":"asdfa",
"gender":"M",
"dob":"1982-04-02T00:00:00",
"phoneNumbers":[
{
"customerId":159,
"category":1,
"number":"+441500000007",
"isPreferred":true,
"createdAt":"2019-07-13T17:20:06.4722858Z",
"updatedAt":null
}
],
"emails":[
{
"customerId":159,
"emailAddress":"[email protected]",
"isSelected":true,
"createdAt":"2019-07-13T17:20:06.4722858Z",
"updatedAt":null
}
],
"addresses":[
{
"customerId":159,
"addressLine1":null,
"addressLine2":null,
"addressLine3":null,
"town":"asdf",
"county":"",
"postcode":"sadf",
"isSelected":true,
"createdAt":"2019-07-13T17:20:06.4722858Z",
"updatedAt":null
}
],
"communicationPreferences":[
{
"customerId":159,
"communicationType":1,
"name":"Notification",
"isSelected":true,
"createdAt":"2019-07-13T17:20:06.4722858Z",
"updatedAt":null
},
{
"customerId":159,
"communicationType":2,
"name":"TextMessage",
"isSelected":false,
"createdAt":"2019-07-13T17:20:06.4722858Z",
"updatedAt":null
},
{
"customerId":159,
"communicationType":3,
"name":"Email",
"isSelected":false,
"createdAt":"2019-07-13T17:20:06.4722858Z",
"updatedAt":null
},
{
"customerId":159,
"communicationType":4,
"name":"InPlatformMessage",
"isSelected":false,
"createdAt":"2019-07-13T17:20:06.4722858Z",
"updatedAt":null
}
],
"username":"[email protected]",
"password":null,
"passwordUpdatedAt":"2019-07-13T17:20:06.4722858Z",
"customerGuid":"f573734a-880d-4829-bc98-da2393b69edc",
"countryCode":"USA",
"currencyCode":"USD",
"languageCode":"en",
"timeZoneCode":"GMT",
"oddsDisplay":5,
"bonusCode":"",
"referrer":"",
"customerCustomFieldDtos":null,
"isActive":false,
"status":0,
"createdAt":"2019-07-13T17:20:06.4722858Z",
"updatedAt":null
},
"errors":null
}
{
"success":false,
"message":"Unable to create customer!",
"payload":null,
"errors":[
{
"errorCode":118,
"errorMessage":"Invalid currency!"
},
{
"errorCode":119,
"errorMessage":"Invalid language!"
}
]
}
{
"success":false,
"message":"Unable to create customer!",
"payload":null,
"errors":[
{
"errorCode":117,
"errorMessage":"Invalid country!"
},
{
"errorCode":118,
"errorMessage":"Invalid currency!"
},
{
"errorCode":119,
"errorMessage":"Invalid language!"
}
]
}
{
"success":false,
"message":"Unable to create customer!",
"payload":null,
"errors":[
{
"errorCode":117,
"errorMessage":"Invalid country!"
},
{
"errorCode":119,
"errorMessage":"Invalid language!"
},
{
"errorCode":120,
"errorMessage":"Invalid timezone!"
}
]
}
{
"success":false,
"message":"Unable to create customer!",
"payload":null,
"errors":[
{
"errorCode":121,
"errorMessage":"Customer already exists!"
}
]
}
(sending password as Password123)
{
"success":false,
"message":"Error while creating customer!",
"payload":null,
"errors":null
}
{
"success":true,
"message":"Success!",
"errors":null
}
{
"success":false,
"message":"Fail",
"errors":{
"errorCode":122,
"errorMessage":"Email not found!"
}
}
{
"success":true,
"message":"Success!",
"errors":null
}
{
"success":false,
"message":"Fail",
"errors":{
"errorCode":122,
"errorMessage":"Email not found!"
}
}
Registration Change Email Address Success
{
"success":true,
"message":"Success!",
"errors":null
}
{
"success":false,
"message":"Fail",
"errors":{
"errorCode":129,
"errorMessage":"Undefined customer!"
}
}
{
"success":true,
"message":"Success!",
"errors":null
}
Registration Confirm Email Failure
{
"success":false,
"message":"Fail",
"errors":{
"errorCode":122,
"errorMessage":"Email not found!"
}
}
{
"success":true,
"message":"Success",
"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImNhcmxvcy5jYXNhbGljY2hpb0BnbWFpbC5jb20iLCJuYW1laWQiOiJiMjYzZThiMS1jYjUxLTQ4YmItYjEwOS1iNjRiMGQxZTY5NGYiLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3VzZXJkYXRhIjoie1wiVGVuYW50SWRcIjozMSxcIlRlbmFudFBsYXRmb3JtUHJvZHVjdE1hcEd1aWRcIjpcIjUwMDQ2OTM1LUM3Q0EtNEFCOS1CNTA0LTkwMDBENUE1MjkwNlwiLFwiQnJhbmROYW1lXCI6bnVsbCxcIkRlZmF1bHRMYW5ndWFnZUNvZGVcIjpudWxsLFwiRGVmYXVsdExhbmd1YWdlTmFtZVwiOm51bGwsXCJBY3RpdmVEb21haW5OYW1lXCI6XCJjdXN0b21lci1tYW5hZ2VtZW50LXNlcnZpY2UtYXBpLnRvdGFsY29kaW5nLXRlc3QxLmNvbVwiLFwiQ3VzdG9tZXJJZFwiOjAsXCJDdXN0b21lckd1aWRcIjpcImIyNjNlOGIxLWNiNTEtNDhiYi1iMTA5LWI2NGIwZDFlNjk0ZlwiLFwiVXNlck5hbWVcIjpcImNhcmxvcy5jYXNhbGljY2hpb0BnbWFpbC5jb21cIixcIkRlc2NyaXB0aW9uXCI6bnVsbH0iLCJuYmYiOjE1NjMwNjU1ODksImV4cCI6MTU2MzA2OTE4OSwiaWF0IjoxNTYzMDY1NTg5LCJpc3MiOiJUb3RhbENvZGluZyJ9.3A7__HEs9bX9Xh10Yj7nFLWiFO4vjWziR_e4vkFRc0E",
"errors":null
}
{
"success":false,
"token":null,
"loginFailureReason":null
}
{
"success":false,
"token":null,
"loginFailureReason":{
"errorCode":"3",
"errorMessage":"Other"
}
}
{
"success":false,
"message":"Fail",
"token":null,
"errors":{
"errorCode":4,
"errorMessage":"Other"
}
}
{
"success":true,
"message":"Success",
"errors":null
}
{
"success":false,
"message":"Fail",
"errors":{
"errorCode":129,
"errorMessage":"Undefined customer!"
}
}
{
"success":true,
"message":"Success",
"errors":null
}
{
"success":false,
"message":"Fail",
"errors":{
"errorCode":129,
"errorMessage":"Undefined customer!"
}
}
{
"success":true,
"message":"Success",
"errors":null
}
{
"success":true,
"message":"Success",
"errors":null
}
{
"success":false,
"message":"Fail",
"errors":{
"errorCode":129,
"errorMessage":"Undefined customer!"
}
}
{
"success":true,
"message":"Success! Verification code: 373215",
"errors":null
}
{
"success":false,
"message":"Fail",
"errors":{
"errorCode":129,
"errorMessage":"Undefined customer!"
}
}
{
"success":true,
"message":"Success",
"errors":null
}
{
"success":false,
"message":"Fail",
"errors":null
}
{
"success":true,
"message":"Success",
"errors":null
}
{
"success":false,
"message":"Fail",
"errors":{
"errorCode":129,
"errorMessage":"Undefined customer!"
}
}