From 98114cf816c3086c59f6dcc482a3f226739ca6f5 Mon Sep 17 00:00:00 2001 From: Dmitry Mamontov Date: Mon, 2 Feb 2015 14:36:40 +0300 Subject: [PATCH] New method --- README.md | 71 +- RetailCrm.sln | 20 - RetailCrm/ApiClient.cs | 755 +++++++++++++++++++ RetailCrm/ApiException.cs | 21 - RetailCrm/CurlException.cs | 21 - RetailCrm/Exceptions/InvalidJsonException.cs | 21 + RetailCrm/Extra/Tools.cs | 94 +++ RetailCrm/Http/Client.cs | 111 +++ RetailCrm/Properties/AssemblyInfo.cs | 36 - RetailCrm/Response/ApiResponse.cs | 94 +++ RetailCrm/RestApi.cs | 708 ----------------- RetailCrm/RetailCrm.csproj | 68 -- RetailCrm/packages.config | 4 - lib/RetailCrm.dll | Bin 15360 -> 19456 bytes 14 files changed, 1138 insertions(+), 886 deletions(-) delete mode 100644 RetailCrm.sln create mode 100644 RetailCrm/ApiClient.cs delete mode 100644 RetailCrm/ApiException.cs delete mode 100644 RetailCrm/CurlException.cs create mode 100644 RetailCrm/Exceptions/InvalidJsonException.cs create mode 100644 RetailCrm/Extra/Tools.cs create mode 100644 RetailCrm/Http/Client.cs delete mode 100644 RetailCrm/Properties/AssemblyInfo.cs create mode 100644 RetailCrm/Response/ApiResponse.cs delete mode 100644 RetailCrm/RestApi.cs delete mode 100644 RetailCrm/RetailCrm.csproj delete mode 100644 RetailCrm/packages.config diff --git a/README.md b/README.md index ca3d6ca..0848ae3 100644 --- a/README.md +++ b/README.md @@ -30,16 +30,71 @@ Constructor arguments are: ``` csharp +ApiClient api; string url, key; -Dictionary orderTypes = new Dictionary(); +try +{ + api = new ApiClient(url, key); +} +catch (WebException e) +{ + System.Console.WriteLine(e.ToString()); +} -RestApi api = new RestApi(url, key); -try { - orderTypes = api.orderTypesList(); -} catch (ApiException e) { - Console.WriteLine(ex.Message); -} catch (CurlException e) { - Console.WriteLine(ex.Message); +Dictionary tmpOrder = new Dictionary(){ + {"number", "example"}, + {"externalId", "example"}, + {"createdAt", DateTime.Now.ToString("Y-m-d H:i:s")}, + {"discount", 50}, + {"phone", "89263832233"}, + {"email", "vshirokov@gmail.com"}, + {"customerComment", "example"}, + {"customFields", new Dictionary(){ + {"reciever_phone", "example"}, + {"reciever_name", "example"}, + {"ext_number", "example"} + } + }, + {"contragentType", "individual"}, + {"orderType", "eshop-individual"}, + {"orderMethod", "app"}, + {"customerId", "555"}, + {"managerId", 8}, + {"items", new Dictionary(){ + {"0", new Dictionary(){ + {"initialPrice", 100}, + {"quantity", 1}, + {"productId", 55}, + {"productName", "example"} + } + } + } + }, + {"delivery", new Dictionary(){ + {"code", "courier"}, + {"date", DateTime.Now.ToString("Y-m-d")}, + {"address", new Dictionary(){ + {"text", "exampleing"} + } + } + } + } + }; + +ApiResponse response = null; +try +{ + response = api.ordersEdit(order); +} +catch (WebException e) +{ + System.Console.WriteLine(e.ToString()); +} + +if (response.isSuccessful() && 201 == response["statusCosde"]) { + System.Console.WriteLine("Заказ успешно создан. ID заказа в retailCRM = " + response["id"]); +} else { + System.Console.WriteLine("Ошибка создания заказа: [Статус HTTP-ответа " + response["statusCosde"] + "] " + response["errorMsg"]); } ``` \ No newline at end of file diff --git a/RetailCrm.sln b/RetailCrm.sln deleted file mode 100644 index 9386f3f..0000000 --- a/RetailCrm.sln +++ /dev/null @@ -1,20 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 2012 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RetailCrm", "IntaroCrm\RetailCrm.csproj", "{1C407E40-0B79-4593-AC79-03BA8DD76DD1}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {1C407E40-0B79-4593-AC79-03BA8DD76DD1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1C407E40-0B79-4593-AC79-03BA8DD76DD1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1C407E40-0B79-4593-AC79-03BA8DD76DD1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1C407E40-0B79-4593-AC79-03BA8DD76DD1}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal diff --git a/RetailCrm/ApiClient.cs b/RetailCrm/ApiClient.cs new file mode 100644 index 0000000..0e2ad76 --- /dev/null +++ b/RetailCrm/ApiClient.cs @@ -0,0 +1,755 @@ +using Newtonsoft.Json; +using RetailCrm.Http; +using RetailCrm.Response; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace RetailCrm +{ + public class ApiClient + { + private const string apiVersion = "v3"; + protected Client client; + + /// + /// Site code + /// + protected string siteCode; + + /// + /// ApiClient creating + /// + /// + /// + /// + public ApiClient(string url, string apiKey, string site = "") + { + if ("/" != url.Substring(url.Length - 1, 1)) + { + url += "/"; + } + + url += "api/" + apiVersion; + + client = new Client(url, new Dictionary() { { "apiKey", apiKey } }); + siteCode = site; + } + + /// + /// Create a order + /// + /// + /// + /// ApiResponse + public ApiResponse ordersCreate(Dictionary order, string site = "") + { + if (order.Count < 1) + { + throw new ArgumentException("Parameter `order` must contains a data"); + } + + return client.makeRequest( + "/orders/create", + Client.METHOD_POST, + this.fillSite( + site, + new Dictionary() { + { "order", JsonConvert.SerializeObject(order) } + } + ) + ); + } + + /// + /// Edit a order + /// + /// + /// + /// + /// ApiResponse + public ApiResponse ordersEdit(Dictionary order, string by = "externalId", string site = "") + { + if (order.Count < 1) + { + throw new ArgumentException("Parameter `order` must contains a data"); + } + + checkIdParameter(by); + + if (order.ContainsKey(by) == false) + { + throw new ArgumentException("Order array must contain the \"" + by + "\" parameter"); + } + + + return client.makeRequest( + "/orders/" + order[by] + "/edit", + Client.METHOD_POST, + this.fillSite( + site, + new Dictionary() { + { "order", JsonConvert.SerializeObject(order) }, + { "by", by } + } + ) + ); + } + + /// + /// Upload array of the orders + /// + /// + /// + /// ApiResponse + public ApiResponse ordersUpload(Dictionary orders, string site = "") + { + if (orders.Count < 1) + { + throw new ArgumentException("Parameter `order` must contains a data"); + } + + return client.makeRequest( + "/orders/upload", + Client.METHOD_POST, + this.fillSite( + site, + new Dictionary() { + { "orders", JsonConvert.SerializeObject(orders) } + } + ) + ); + } + + /// + /// Get order by id or externalId + /// + /// + /// + /// + /// ApiResponse + public ApiResponse ordersGet(string id, string by = "externalId", string site = "") + { + checkIdParameter(by); + + return client.makeRequest( + "/orders/" + id, + Client.METHOD_GET, + this.fillSite( + site, + new Dictionary() { + { "by", by } + } + ) + ); + } + + /// + /// Returns a orders history + /// + /// + /// + /// + /// + /// + /// ApiResponse + public ApiResponse ordersHistory( + DateTime? startDate = null, + DateTime? endDate = null, + int limit = 100, + int offset = 0, + bool skipMyChanges = true + ) + { + Dictionary parameters = new Dictionary(); + + if (startDate != null) + { + parameters.Add("startDate", startDate.Value.ToString("Y-m-d H:i:s")); + } + if (endDate != null) + { + parameters.Add("endDate", endDate.Value.ToString("Y-m-d H:i:s")); + } + if (limit > 0) + { + parameters.Add("limit", limit); + } + if (offset > 0) + { + parameters.Add("offset", offset); + } + if (skipMyChanges == true) + { + parameters.Add("skipMyChanges", skipMyChanges); + } + + return client.makeRequest("/orders/history", Client.METHOD_GET, parameters); + } + + /// + /// Returns filtered orders list + /// + /// + /// + /// + /// ApiResponse + public ApiResponse ordersList(Dictionary filter = null, int page = 0, int limit = 0) + { + Dictionary parameters = new Dictionary(); + + if (filter.Count > 0) + { + parameters.Add("filter", filter); + } + if (page > 0) + { + parameters.Add("page", page); + } + if (limit > 0) + { + parameters.Add("limit", limit); + } + + return client.makeRequest("/orders", Client.METHOD_GET, parameters); + } + + /// + /// Returns statuses of the orders + /// + /// + /// + /// ApiResponse + public ApiResponse ordersStatuses(Dictionary ids = null, Dictionary externalIds = null) + { + Dictionary parameters = new Dictionary(); + + if (ids.Count > 0) + { + parameters.Add("ids", ids); + } + if (externalIds.Count > 0) + { + parameters.Add("externalIds", externalIds); + } + + return client.makeRequest("/orders/statuses", Client.METHOD_GET, parameters); + } + + /// + /// Save order IDs' (id and externalId) association in the CRM + /// + /// + /// ApiResponse + public ApiResponse ordersFixExternalId(Dictionary ids) + { + if (ids.Count < 1) + { + throw new ArgumentException("Method parameter must contains at least one IDs pair"); + } + + return client.makeRequest( + "/orders/fix-external-ids", + Client.METHOD_POST, + new Dictionary() { + { "orders", JsonConvert.SerializeObject(ids) } + } + ); + } + + /// + /// Create a customer + /// + /// + /// + /// ApiResponse + public ApiResponse customersCreate(Dictionary customer, string site = "") + { + if (customer.Count < 1) + { + throw new ArgumentException("Parameter `customer` must contains a data"); + } + + return client.makeRequest( + "/customers/create", + Client.METHOD_POST, + this.fillSite( + site, + new Dictionary() { + { "customer", JsonConvert.SerializeObject(customer) } + } + ) + ); + } + + /// + /// Edit a customer + /// + /// + /// + /// + /// ApiResponse + public ApiResponse customersEdit(Dictionary customer, string by = "externalId", string site = "") + { + if (customer.Count < 1) + { + throw new ArgumentException("Parameter `customer` must contains a data"); + } + + checkIdParameter(by); + + if (customer.ContainsKey(by) == false) + { + throw new ArgumentException("Customer array must contain the \"" + by + "\" parameter"); + } + + + return client.makeRequest( + "/customers/" + customer[by] + "/edit", + Client.METHOD_POST, + this.fillSite( + site, + new Dictionary() { + { "customer", JsonConvert.SerializeObject(customer) }, + { "by", by } + } + ) + ); + } + + /// + /// Upload array of the customers + /// + /// + /// + /// ApiResponse + public ApiResponse customersUpload(Dictionary customers, string site = "") + { + if (customers.Count < 1) + { + throw new ArgumentException("Parameter `customers` must contains a data"); + } + + return client.makeRequest( + "/customers/upload", + Client.METHOD_POST, + this.fillSite( + site, + new Dictionary() { + { "customers", JsonConvert.SerializeObject(customers) } + } + ) + ); + } + + /// + /// Get customer by id or externalId + /// + /// + /// + /// + /// ApiResponse + public ApiResponse customersGet(string id, string by = "externalId", string site = "") + { + checkIdParameter(by); + + return client.makeRequest( + "/customers/" + id, + Client.METHOD_GET, + this.fillSite( + site, + new Dictionary() { + { "by", by } + } + ) + ); + } + + /// + /// Returns filtered customers list + /// + /// + /// + /// + /// ApiResponse + public ApiResponse customersList(Dictionary filter = null, int page = 0, int limit = 0) + { + Dictionary parameters = new Dictionary(); + if (filter.Count > 0) + { + parameters.Add("filter", filter); + } + if (page > 0) + { + parameters.Add("page", page); + } + if (limit > 0) + { + parameters.Add("limit", limit); + } + + return client.makeRequest("/customers", Client.METHOD_GET, parameters); + } + + /// + /// Save customer IDs' (id and externalId) association in the CRM + /// + /// + /// ApiResponse + public ApiResponse customersFixExternalIds(Dictionary ids) + { + if (ids.Count < 1) + { + throw new ArgumentException("Method parameter must contains at least one IDs pair"); + } + + return client.makeRequest( + "/customers/fix-external-ids", + Client.METHOD_POST, + new Dictionary() { + { "customers", JsonConvert.SerializeObject(ids) } + } + ); + } + + /// + /// Returns deliveryServices list + /// + /// ApiResponse + public ApiResponse deliveryServicesList() + { + return client.makeRequest("/reference/delivery-services", Client.METHOD_GET); + } + + /// + /// Returns deliveryTypes list + /// + /// ApiResponse + public ApiResponse deliveryTypesList() + { + return client.makeRequest("/reference/delivery-types", Client.METHOD_GET); + } + + /// + /// Returns orderMethods list + /// + /// ApiResponse + public ApiResponse orderMethodsList() + { + return client.makeRequest("/reference/order-methods", Client.METHOD_GET); + } + + /// + /// Returns orderTypes list + /// + /// ApiResponse + public ApiResponse orderTypesList() + { + return client.makeRequest("/reference/order-types", Client.METHOD_GET); + } + + /// + /// Returns paymentStatuses list + /// + /// ApiResponse + public ApiResponse paymentStatusesList() + { + return client.makeRequest("/reference/payment-statuses", Client.METHOD_GET); + } + + /// + /// Returns paymentTypes list + /// + /// ApiResponse + public ApiResponse paymentTypesList() + { + return client.makeRequest("/reference/payment-types", Client.METHOD_GET); + } + + /// + /// Returns productStatuses list + /// + /// ApiResponse + public ApiResponse productStatusesList() + { + return client.makeRequest("/reference/product-statuses", Client.METHOD_GET); + } + + /// + /// Returns statusGroups list + /// + /// ApiResponse + public ApiResponse statusGroupsList() + { + return client.makeRequest("/reference/status-groups", Client.METHOD_GET); + } + + /// + /// Returns statuses list + /// + /// ApiResponse + public ApiResponse statusesList() + { + return client.makeRequest("/reference/statuses", Client.METHOD_GET); + } + + /// + /// Returns sites list + /// + /// ApiResponse + public ApiResponse sitesList() + { + return client.makeRequest("/reference/sites", Client.METHOD_GET); + } + + /// + /// Edit deliveryService + /// + /// + /// ApiResponse + public ApiResponse deliveryServicesEdit(Dictionary data) + { + if (data.ContainsKey("code") == false) + { + throw new ArgumentException("Data must contain \"code\" parameter"); + } + + return client.makeRequest( + "/reference/delivery-services/" + data["code"] + "/edit", + Client.METHOD_POST, + new Dictionary() { + { "deliveryService", JsonConvert.SerializeObject(data) } + } + ); + } + + /// + /// Edit deliveryType + /// + /// + /// ApiResponse + public ApiResponse deliveryTypesEdit(Dictionary data) + { + if (data.ContainsKey("code") == false) + { + throw new ArgumentException("Data must contain \"code\" parameter"); + } + + return client.makeRequest( + "/reference/delivery-types/" + data["code"] + "/edit", + Client.METHOD_POST, + new Dictionary() { + { "deliveryType", JsonConvert.SerializeObject(data) } + } + ); + } + + /// + /// Edit orderMethod + /// + /// + /// ApiResponse + public ApiResponse orderMethodsEdit(Dictionary data) + { + if (data.ContainsKey("code") == false) + { + throw new ArgumentException("Data must contain \"code\" parameter"); + } + + return client.makeRequest( + "/reference/order-methods/" + data["code"] + "/edit", + Client.METHOD_POST, + new Dictionary() { + { "orderMethod", JsonConvert.SerializeObject(data) } + } + ); + } + + /// + /// Edit orderType + /// + /// + /// ApiResponse + public ApiResponse orderTypesEdit(Dictionary data) + { + if (data.ContainsKey("code") == false) + { + throw new ArgumentException("Data must contain \"code\" parameter"); + } + + return client.makeRequest( + "/reference/order-types/" + data["code"] + "/edit", + Client.METHOD_POST, + new Dictionary() { + { "orderType", JsonConvert.SerializeObject(data) } + } + ); + } + + /// + /// Edit paymentStatus + /// + /// + /// ApiResponse + public ApiResponse paymentStatusesEdit(Dictionary data) + { + if (data.ContainsKey("code") == false) + { + throw new ArgumentException("Data must contain \"code\" parameter"); + } + + return client.makeRequest( + "/reference/payment-statuses/" + data["code"] + "/edit", + Client.METHOD_POST, + new Dictionary() { + { "paymentStatus", JsonConvert.SerializeObject(data) } + } + ); + } + + /// + /// Edit paymentType + /// + /// + /// ApiResponse + public ApiResponse paymentTypesEdit(Dictionary data) + { + if (data.ContainsKey("code") == false) + { + throw new ArgumentException("Data must contain \"code\" parameter"); + } + + return client.makeRequest( + "/reference/payment-types/" + data["code"] + "/edit", + Client.METHOD_POST, + new Dictionary() { + { "paymentType", JsonConvert.SerializeObject(data) } + } + ); + } + + /// + /// Edit productStatus + /// + /// + /// ApiResponse + public ApiResponse productStatusesEdit(Dictionary data) + { + if (data.ContainsKey("code") == false) + { + throw new ArgumentException("Data must contain \"code\" parameter"); + } + + return client.makeRequest( + "/reference/product-statuses/" + data["code"] + "/edit", + Client.METHOD_POST, + new Dictionary() { + { "productStatus", JsonConvert.SerializeObject(data) } + } + ); + } + + /// + /// Edit order status + /// + /// + /// ApiResponse + public ApiResponse statusesEdit(Dictionary data) + { + if (data.ContainsKey("code") == false) + { + throw new ArgumentException("Data must contain \"code\" parameter"); + } + + return client.makeRequest( + "/reference/statuses/" + data["code"] + "/edit", + Client.METHOD_POST, + new Dictionary() { + { "status", JsonConvert.SerializeObject(data) } + } + ); + } + + /// + /// Edit site + /// + /// + /// ApiResponse + public ApiResponse sitesEdit(Dictionary data) + { + if (data.ContainsKey("code") == false) + { + throw new ArgumentException("Data must contain \"code\" parameter"); + } + + return client.makeRequest( + "/reference/sites/" + data["code"] + "/edit", + Client.METHOD_POST, + new Dictionary() { + { "site", JsonConvert.SerializeObject(data) } + } + ); + } + + /// + /// Update CRM basic statistic + /// + /// ApiResponse + public ApiResponse statisticUpdate() + { + return client.makeRequest("/statistic/update", Client.METHOD_GET); + } + + /// + /// Return current site + /// + /// string + public string getSite() + { + return this.siteCode; + } + + /// + /// Return current site + /// + public void setSite(string site) + { + this.siteCode = site; + } + + /// + /// Check ID parameter + /// + /// + protected void checkIdParameter(string by) + { + string[] allowedForBy = new string[]{"externalId", "id"}; + if (allowedForBy.Contains(by) == false) + { + throw new ArgumentException("Value \"" + by + "\" for parameter \"by\" is not valid. Allowed values are " + String.Join(", ", allowedForBy)); + } + } + + /// + /// Fill params by site value + /// + /// + /// + /// Dictionary + protected Dictionary fillSite(string site, Dictionary param) + { + if (site.Length > 1) + { + param.Add("site", site); + } + else if (siteCode.Length > 1) + { + param.Add("site", siteCode); + } + + return param; + } + } +} diff --git a/RetailCrm/ApiException.cs b/RetailCrm/ApiException.cs deleted file mode 100644 index 3bcf0f5..0000000 --- a/RetailCrm/ApiException.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; - -namespace RetailCrm -{ - public class ApiException : Exception - { - public ApiException() - { - } - - public ApiException(string message) - : base(message) - { - } - - public ApiException(string message, Exception inner) - : base(message, inner) - { - } - } -} diff --git a/RetailCrm/CurlException.cs b/RetailCrm/CurlException.cs deleted file mode 100644 index fff3862..0000000 --- a/RetailCrm/CurlException.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; - -namespace RetailCrm -{ - public class CurlException : Exception - { - public CurlException() - { - } - - public CurlException(string message) - : base(message) - { - } - - public CurlException(string message, Exception inner) - : base(message, inner) - { - } - } -} diff --git a/RetailCrm/Exceptions/InvalidJsonException.cs b/RetailCrm/Exceptions/InvalidJsonException.cs new file mode 100644 index 0000000..12f19d0 --- /dev/null +++ b/RetailCrm/Exceptions/InvalidJsonException.cs @@ -0,0 +1,21 @@ +using System; + +namespace RetailCrm.Exceptions +{ + public class InvalidJsonException : Exception + { + public InvalidJsonException() + { + } + + public InvalidJsonException(string message) + : base(message) + { + } + + public InvalidJsonException(string message, Exception inner) + : base(message, inner) + { + } + } +} diff --git a/RetailCrm/Extra/Tools.cs b/RetailCrm/Extra/Tools.cs new file mode 100644 index 0000000..f464251 --- /dev/null +++ b/RetailCrm/Extra/Tools.cs @@ -0,0 +1,94 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace RetailCrm.Extra +{ + class Tools + { + public static string httpBuildQuery(Dictionary data) + { + if (data is Dictionary == false) + { + return String.Empty; + } + + var parts = new List(); + HandleItem(data, parts); + return String.Join("&", parts); + } + + private static void HandleItem(object data, List parts, string prefix = "") + { + if (data == null) return; + + if (data is Dictionary) + { + parts.Add(FormatDictionary((Dictionary)data, prefix)); + } + else + { + parts.Add(String.IsNullOrEmpty(data.ToString()) ? String.Empty : String.Format("{0}={1}", prefix, data.ToString())); + } + } + + private static string FormatDictionary(Dictionary obj, string prefix = "") + { + var parts = new List(); + foreach (KeyValuePair kvp in obj) + { + string newPrefix = string.IsNullOrEmpty(prefix) ? + String.Format("{0}{1}", prefix, kvp.Key) : + String.Format("{0}[{1}]", prefix, kvp.Key); + HandleItem(kvp.Value, parts, newPrefix); + } + + return String.Join("&", parts); + } + + public static Dictionary jsonDecode(string json) + { + return jsonObjectToDictionary((Dictionary)JsonConvert.DeserializeObject>(json)); + } + + private static Dictionary jsonObjectToDictionary(Dictionary data) + { + Dictionary result = new Dictionary(); + foreach (KeyValuePair kvp in data) + { + System.Console.WriteLine(kvp.Key.ToString()); + System.Console.WriteLine(kvp.Value.ToString()); + System.Console.ReadLine(); + object valueObj = kvp.Value; + string value = String.Empty; + + if (valueObj.GetType() == typeof(JArray)) + { + string tmpValue = JsonConvert.SerializeObject(((JArray)valueObj).ToArray()); + value = tmpValue.Replace("[", "{"); + value = value.Replace("]", "}"); + } + else + { + value = valueObj.ToString(); + } + System.Console.WriteLine(value); + System.Console.ReadLine(); + if (value != "") + { + if (valueObj.GetType() == typeof(JObject) || valueObj.GetType() == typeof(JArray)) + { + valueObj = jsonObjectToDictionary((Dictionary)JsonConvert.DeserializeObject>(value)); + } + result.Add(kvp.Key.ToString(), valueObj); + } + } + return result; + } + } +} diff --git a/RetailCrm/Http/Client.cs b/RetailCrm/Http/Client.cs new file mode 100644 index 0000000..9cb9810 --- /dev/null +++ b/RetailCrm/Http/Client.cs @@ -0,0 +1,111 @@ +using RetailCrm.Extra; +using RetailCrm.Response; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; + +namespace RetailCrm.Http +{ + /// + /// HTTP client + /// + public class Client + { + public const string METHOD_GET = "GET"; + public const string METHOD_POST = "POST"; + private const string USER_AGENT = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)"; + private const string CONTENT_TYPE = "application/x-www-form-urlencoded"; + + protected string url; + protected Dictionary defaultParameter; + + /// + /// Creating HTTP client + /// + /// + /// + public Client(string apiUrl, Dictionary parameters = null) + { + if (apiUrl.IndexOf("https://") == -1) + { + throw new ArgumentException("API schema requires HTTPS protocol"); + } + + url = apiUrl; + defaultParameter = parameters; + } + + /// + /// Make HTTP request + /// + /// + /// + /// + /// + /// + public ApiResponse makeRequest(string path, string method, Dictionary parameters = null, int timeout = 30) + { + string[] allowedMethods = new string[] { METHOD_GET, METHOD_POST }; + if (allowedMethods.Contains(method) == false) + { + throw new ArgumentException("Method \"" + method + "\" is not valid. Allowed methods are " + String.Join(", ", allowedMethods)); + } + if (parameters == null) { + parameters = new Dictionary(); + } + parameters = defaultParameter.Union(parameters).ToDictionary(k => k.Key, v => v.Value); + path = url + path; + string httpQuery = Tools.httpBuildQuery(parameters); + + if (method.Equals(METHOD_GET) && parameters.Count > 0) + { + path += "?" + httpQuery; + } + + Exception exception = null; + + HttpWebRequest request = (HttpWebRequest) WebRequest.Create(path); + request.Method = method; + + if (method.Equals(METHOD_POST)) + { + UTF8Encoding encoding = new UTF8Encoding(); + byte[] bytes = encoding.GetBytes(httpQuery); + request.ContentLength = bytes.Length; + request.ContentType = CONTENT_TYPE; + request.UserAgent = USER_AGENT; + + Stream post = request.GetRequestStream(); + post.Write(bytes, 0, bytes.Length); + post.Flush(); + post.Close(); + } + + HttpWebResponse response = null; + try + { + response = (HttpWebResponse)request.GetResponse(); + } + catch (WebException ex) + { + response = (HttpWebResponse)ex.Response; + exception = ex; + } + + if (request == null || response == null) + { + throw new WebException(exception.ToString(), exception); + } + + StreamReader reader = new StreamReader((Stream) response.GetResponseStream()); + string responseBody = reader.ReadToEnd(); + int statusCode = (int) response.StatusCode; + + return new ApiResponse(statusCode, responseBody); + } + } +} diff --git a/RetailCrm/Properties/AssemblyInfo.cs b/RetailCrm/Properties/AssemblyInfo.cs deleted file mode 100644 index f84f137..0000000 --- a/RetailCrm/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// Управление общими сведениями о сборке осуществляется с помощью -// набора атрибутов. Измените значения этих атрибутов, чтобы изменить сведения, -// связанные со сборкой. -[assembly: AssemblyTitle("IntaroCrm")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("IntaroCrm")] -[assembly: AssemblyCopyright("Copyright © 2014")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Параметр ComVisible со значением FALSE делает типы в сборке невидимыми -// для COM-компонентов. Если требуется обратиться к типу в этой сборке через -// COM, задайте атрибуту ComVisible значение TRUE для этого типа. -[assembly: ComVisible(false)] - -// Следующий GUID служит для идентификации библиотеки типов, если этот проект будет видимым для COM -[assembly: Guid("9a3f29a5-d878-430c-b7ee-8548d4d03f43")] - -// Сведения о версии сборки состоят из следующих четырех значений: -// -// Основной номер версии -// Дополнительный номер версии -// Номер построения -// Редакция -// -// Можно задать все значения или принять номер построения и номер редакции по умолчанию, -// используя "*", как показано ниже: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/RetailCrm/Response/ApiResponse.cs b/RetailCrm/Response/ApiResponse.cs new file mode 100644 index 0000000..6344f18 --- /dev/null +++ b/RetailCrm/Response/ApiResponse.cs @@ -0,0 +1,94 @@ +using Newtonsoft.Json; +using RetailCrm.Exceptions; +using RetailCrm.Extra; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace RetailCrm.Response +{ + /// + /// Response from retailCRM API + /// + public class ApiResponse + { + /// + /// HTTP response status code + /// + protected int statusCode; + + /// + /// Response + /// + protected Dictionary response; + + /// + /// Creating ApiResponse + /// + /// + /// + public ApiResponse(int statusCode, string responseBody = null) + { + this.statusCode = statusCode; + + if (responseBody != null && responseBody.Length > 0) + { + Dictionary response = new Dictionary(); + try + { + response = Tools.jsonDecode(responseBody); + } + catch (JsonReaderException e) + { + throw new InvalidJsonException("Invalid JSON in the API response body. " + e.ToString()); + } + + this.response = response; + } + } + + /// + /// Return HTTP response status code + /// + /// int + public int getStatusCode() + { + return this.statusCode; + } + + /// + /// HTTP request was successful + /// + /// boolean + public bool isSuccessful() + { + return this.statusCode < 400; + } + + /// + /// Return response + /// + /// Dictionary + public object this[string code] + { + get { + if (this.response.ContainsKey(code)) + { + return this.response[code]; + } + else + { + return new Dictionary(); + } + } + set + { + throw new ArgumentException("Property \"" + code + "\" is not writable"); + } + } + + + } +} diff --git a/RetailCrm/RestApi.cs b/RetailCrm/RestApi.cs deleted file mode 100644 index 092c632..0000000 --- a/RetailCrm/RestApi.cs +++ /dev/null @@ -1,708 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Text; -using System.Threading.Tasks; -using Newtonsoft.Json.Linq; -using Newtonsoft.Json; -using System.Collections; - -namespace RetailCrm -{ - public class RestApi - { - protected string apiUrl; - protected string apiKey; - protected string apiVersion = "3"; - protected DateTime generatedAt; - protected Dictionary parameters; - - private string userAgent = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)"; - private string contentType = "application/x-www-form-urlencoded"; - - /// адрес CRM - /// ключ для работы с api - public RestApi(string crmUrl, string crmKey) - { - apiUrl = crmUrl + "/api/v" + apiVersion + "/"; - apiKey = crmKey; - parameters = new Dictionary(); - parameters.Add("apiKey", apiKey); - } - - /// - /// Получение заказа по id - /// - /// идентификатор заказа - /// поиск заказа по id или externalId - /// информация о заказе - public Dictionary orderGet(int id, string by) - { - Dictionary result = new Dictionary(); - string url = apiUrl + "orders/" + id.ToString(); - if (by.Equals("externalId")) - { - parameters.Add("by", by); - } - result = request(url, "GET"); - - return result; - } - - /// - /// Создание заказа - /// - /// информация о заказе - /// - public Dictionary orderCreate(Dictionary order) - { - Dictionary result = new Dictionary(); - string url = apiUrl + "orders/create"; - string dataJson = JsonConvert.SerializeObject(order); - parameters.Add("order", dataJson); - result = request(url, "POST"); - - return result; - } - - /// - /// Изменение заказа - /// - /// информация о заказе - /// - public Dictionary orderEdit(Dictionary order) - { - Dictionary result = new Dictionary(); - string url = apiUrl + "orders/" + order["externalId"].ToString() + "/edit"; - string dataJson = JsonConvert.SerializeObject(order); - parameters.Add("order", dataJson); - result = request(url, "POST"); - - return result; - } - - /// - /// Пакетная загрузка заказов - /// - /// массив заказов - /// - public Dictionary orderUpload(Dictionary orders) - { - Dictionary result = new Dictionary(); - string url = apiUrl + "orders/upload"; - string dataJson = JsonConvert.SerializeObject(orders); - parameters.Add("orders", dataJson); - result = request(url, "POST"); - - if (result.ContainsKey("uploadedOrders") && result != null) - { - return getDictionary(result["uploadedOrders"]); - } - - return result; - } - - /// - /// Обновление externalId у заказов с переданными id - /// - /// массив, содержащий id и externalId заказа - /// - public Dictionary orderFixExternalIds(Dictionary orders) - { - Dictionary result = new Dictionary(); - string url = apiUrl + "orders/fix-external-ids"; - string dataJson = JsonConvert.SerializeObject(orders); - parameters.Add("orders", dataJson); - result = request(url, "POST"); - - return result; - } - - /// - /// Получение последних измененных заказов - /// - /// начальная дата выборки - /// конечная дата - /// ограничение на размер выборки - /// сдвиг - /// массив заказов - public Dictionary orderHistory(DateTime startDate, DateTime endDate, int limit, int offset) - { - Dictionary result = new Dictionary(); - string url = apiUrl + "orders/history"; - parameters.Add("startDate", startDate.ToString()); - parameters.Add("startDate", endDate.ToString()); - parameters.Add("limit", limit.ToString()); - parameters.Add("offset", offset.ToString()); - result = request(url, "GET"); - - return result; - } - - /// - /// Получение клиента по id - /// - /// идентификатор - /// поиск заказа по id или externalId - /// информация о клиенте - public Dictionary customerGet(string id, string by) - { - Dictionary result = new Dictionary(); - string url = apiUrl + "customers/" + id; - if (by.Equals("externalId")) - { - parameters.Add("by", by); - } - result = request(url, "GET"); - - return result; - } - - /// - /// Получение списка клиентов в соответсвии с запросом - /// - /// телефон - /// почтовый адрес - /// фио пользователя - /// ограничение на размер выборки - /// сдвиг - /// массив клиентов - public Dictionary customers(string phone, string email, string fio, int limit, int offset) - { - Dictionary result = new Dictionary(); - string url = apiUrl + "customers"; - parameters.Add("phone", phone); - parameters.Add("email", email); - parameters.Add("fio", fio); - parameters.Add("limit", limit.ToString()); - parameters.Add("offset", offset.ToString()); - result = request(url, "GET"); - - return result; - } - - /// - /// Создание клиента - /// - /// информация о клиенте - /// - public Dictionary customerCreate(Dictionary customer) - { - Dictionary result = new Dictionary(); - string url = apiUrl + "customers/create"; - string dataJson = JsonConvert.SerializeObject(customer); - parameters.Add("customer", dataJson); - result = request(url, "POST"); - - return result; - } - - /// - /// Редактирование клиента - /// - /// информация о клиенте - /// - public Dictionary customerEdit(Dictionary customer) - { - Dictionary result = new Dictionary(); - string url = apiUrl + "customers/" + customer["externalId"].ToString() + "/edit"; - string dataJson = JsonConvert.SerializeObject(customer); - parameters.Add("customer", dataJson); - result = request(url, "POST"); - - return result; - } - - /// - /// Пакетная загрузка клиентов - /// - /// массив клиентов - /// - public Dictionary customerUpload(Dictionary customers) - { - Dictionary result = new Dictionary(); - string url = apiUrl + "customers/upload"; - string dataJson = JsonConvert.SerializeObject(customers); - parameters.Add("customers", dataJson); - result = request(url, "POST"); - - if (result.ContainsKey("uploaded") && result != null) - { - return getDictionary(result["uploaded"]); - } - - return result; - } - - /// - /// Обновление externalId у клиентов с переданными id - /// - /// массив, содержащий id и externalId заказа - /// - public Dictionary customerFixExternalIds(Dictionary customers) - { - Dictionary result = new Dictionary(); - string url = apiUrl + "customers/fix-external-ids"; - string dataJson = JsonConvert.SerializeObject(customers); - parameters.Add("customers", dataJson); - result = request(url, "POST"); - - return result; - } - - /// - /// Получение списка заказов клиента - /// - /// идентификатор клиента - /// начальная дата выборки - /// конечная дата - /// ограничение на размер выборки - /// сдвиг - /// поиск заказа по id или externalId - /// массив заказов - public Dictionary customerOrdersList(string id, DateTime startDate, DateTime endDate, int limit, int offset, string by) - { - Dictionary result = new Dictionary(); - string url = apiUrl + "customers/" + id + "/orders"; - if (by.Equals("externalId")) - { - parameters.Add("by", by); - } - parameters.Add("startDate", startDate.ToString()); - parameters.Add("endDate", endDate.ToString()); - parameters.Add("limit", limit.ToString()); - parameters.Add("offset", offset.ToString()); - result = request(url, "POST"); - - return result; - } - - /// - /// Получение списка типов доставки - /// - /// массив типов доставки - public Dictionary deliveryTypesList() - { - Dictionary result = new Dictionary(); - string url = apiUrl + "reference/delivery-types"; - result = request(url, "GET"); - - return result; - } - - /// - /// Редактирование типа доставки - /// - /// информация о типе доставки - /// - public Dictionary deliveryTypeEdit(Dictionary deliveryType) - { - Dictionary result = new Dictionary(); - string url = apiUrl + "reference/delivery-types/" + deliveryType["code"].ToString() + "/edit"; - string dataJson = JsonConvert.SerializeObject(deliveryType); - parameters.Add("deliveryType", dataJson); - result = request(url, "POST"); - - return result; - } - - /// - /// Получение списка служб доставки - /// - /// массив служб доставки - public Dictionary deliveryServicesList() - { - Dictionary result = new Dictionary(); - string url = apiUrl + "reference/delivery-services"; - result = request(url, "GET"); - - return result; - } - - /// - /// Редактирование службы доставки - /// - /// информация о службе доставки - /// - public Dictionary deliveryServiceEdit(Dictionary deliveryService) - { - Dictionary result = new Dictionary(); - string url = apiUrl + "reference/delivery-services/" + deliveryService["code"].ToString() + "/edit"; - string dataJson = JsonConvert.SerializeObject(deliveryService); - parameters.Add("deliveryService", dataJson); - result = request(url, "POST"); - - return result; - } - - /// - /// Получение списка типов оплаты - /// - /// массив типов оплаты - public Dictionary paymentTypesList() - { - Dictionary result = new Dictionary(); - string url = apiUrl + "reference/payment-types"; - result = request(url, "GET"); - - return result; - } - - /// - /// Редактирование типа оплаты - /// - /// информация о типе оплаты - /// - public Dictionary paymentTypesEdit(Dictionary paymentType) - { - Dictionary result = new Dictionary(); - string url = apiUrl + "reference/payment-types/" + paymentType["code"].ToString() + "/edit"; - string dataJson = JsonConvert.SerializeObject(paymentType); - parameters.Add("paymentType", dataJson); - result = request(url, "POST"); - - return result; - } - - /// - /// Получение списка статусов оплаты - /// - /// массив статусов оплаты - public Dictionary paymentStatusesList() - { - Dictionary result = new Dictionary(); - string url = apiUrl + "reference/payment-statuses"; - result = request(url, "GET"); - - return result; - } - - /// - /// Редактирование статуса оплаты - /// - /// информация о статусе оплаты - /// - public Dictionary paymentStatusesEdit(Dictionary paymentStatus) - { - Dictionary result = new Dictionary(); - string url = apiUrl + "reference/payment-statuses/" + paymentStatus["code"].ToString() + "/edit"; - string dataJson = JsonConvert.SerializeObject(paymentStatus); - parameters.Add("paymentStatus", dataJson); - result = request(url, "POST"); - - return result; - } - - /// - /// Получение списка типов заказа - /// - /// массив типов заказа - public Dictionary orderTypesList() - { - Dictionary result = new Dictionary(); - string url = apiUrl + "reference/order-types"; - result = request(url, "GET"); - - return result; - } - - /// - /// Редактирование типа заказа - /// - /// информация о типе заказа - /// - public Dictionary orderTypesEdit(Dictionary orderType) - { - Dictionary result = new Dictionary(); - string url = apiUrl + "reference/order-types/" + orderType["code"].ToString() + "/edit"; - string dataJson = JsonConvert.SerializeObject(orderType); - parameters.Add("orderType", dataJson); - result = request(url, "POST"); - - return result; - } - - /// - /// Получение списка способов оформления заказа - /// - /// массив способов оформления заказа - public Dictionary orderMethodsList() - { - Dictionary result = new Dictionary(); - string url = apiUrl + "reference/order-methods"; - result = request(url, "GET"); - - return result; - } - - /// - /// Редактирование способа оформления заказа - /// - /// информация о способе оформления заказа - /// - public Dictionary orderMethodsEdit(Dictionary orderMethod) - { - Dictionary result = new Dictionary(); - string url = apiUrl + "reference/order-methods/" + orderMethod["code"].ToString() + "/edit"; - string dataJson = JsonConvert.SerializeObject(orderMethod); - parameters.Add("orderMethod", dataJson); - result = request(url, "POST"); - - return result; - } - - /// - /// Получение списка статусов заказа - /// - /// массив статусов заказа - public Dictionary orderStatusesList() - { - Dictionary result = new Dictionary(); - string url = apiUrl + "reference/statuses"; - result = request(url, "GET"); - - return result; - } - - /// - /// Редактирование статуса заказа - /// - /// информация о статусе заказа - /// - public Dictionary orderStatusEdit(Dictionary status) - { - Dictionary result = new Dictionary(); - string url = apiUrl + "reference/statuses/" + status["code"].ToString() + "/edit"; - string dataJson = JsonConvert.SerializeObject(status); - parameters.Add("status", dataJson); - result = request(url, "POST"); - - return result; - } - - /// - /// Получение списка групп статусов заказа - /// - /// массив групп статусов заказа - public Dictionary orderStatusGroupsList() - { - Dictionary result = new Dictionary(); - string url = apiUrl + "reference/status-groups"; - result = request(url, "GET"); - - return result; - } - - /// - /// Обновление статистики - /// - /// статус выполненного обновления - public Dictionary statisticUpdate() - { - Dictionary result = new Dictionary(); - string url = apiUrl + "statistic/update"; - result = request(url, "GET"); - - return result; - } - - /// дата генерации - public DateTime getGeneratedAt() - { - return generatedAt; - } - - /// - /// - /// - protected Dictionary request(string url, string method) - { - Dictionary data = new Dictionary(); - string urlParameters = httpBuildQuery(parameters); - - if (method.Equals("GET") && urlParameters.Length > 0) - { - url += "?" + urlParameters; - } - - Exception exception = null; - - HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url); - - request.Method = method; - - if (method.Equals("POST")) - { - UTF8Encoding encoding = new UTF8Encoding(); - byte[] postBytes = encoding.GetBytes(urlParameters); - - request.ContentType = contentType; - request.ContentLength = postBytes.Length; - request.UserAgent = userAgent; - - Stream postStream = request.GetRequestStream(); - postStream.Write(postBytes, 0, postBytes.Length); - postStream.Flush(); - postStream.Close(); - } - - HttpWebResponse response = null; - try - { - response = (HttpWebResponse)request.GetResponse(); - } - catch (WebException ex) - { - response = (HttpWebResponse)ex.Response; - exception = ex; - } - - if (request == null || response == null) - { - throw new CurlException(exception.ToString(), exception); - } - - int statusCode = (int)response.StatusCode; - - Stream dataStream = response.GetResponseStream(); - - StreamReader reader = new StreamReader(dataStream); - string serverResponse = reader.ReadToEnd(); - - parameters.Clear(); - parameters.Add("apiKey", apiKey); - - data = jsonDecode(serverResponse); - - if (data.ContainsKey("generatedAt")) - { - generatedAt = DateTime.ParseExact(data["generatedAt"].ToString(), "Y-m-d H:i:s", - System.Globalization.CultureInfo.InvariantCulture); - data.Remove("generatedAt"); - } - - if (statusCode >= 400 || (data.ContainsKey("success") && !(bool)data["success"])) - { - throw new ApiException(getErrorMessage(data)); - } - - data.Remove("success"); - - if (data.Count == 0) - { - return null; - } - - return data; - } - - /// - /// - protected string getErrorMessage(Dictionary data) - { - string error = ""; - - if (data.ContainsKey("message")) - { - error = data["message"].ToString(); - } - else if (data.ContainsKey("0")) - { - Dictionary sub = getDictionary(data["0"]); - if (sub.ContainsKey("message")) - { - error = sub["message"].ToString(); - } - - } - else if (data.ContainsKey("errorMsg")) - { - error = data["errorMsg"].ToString(); - } - else if (data.ContainsKey("error")) - { - Dictionary sub = getDictionary(data["error"]); - if (sub.ContainsKey("message")) - { - error = sub["message"].ToString(); - } - } - - if (data.ContainsKey("errors")) - { - Dictionary sub = getDictionary(data["errors"]); - foreach (KeyValuePair kvp in data) - { - error += ". " + kvp.Value.ToString(); - } - } - - return error; - } - - /// - /// - public Dictionary getDictionary(object data) - { - Dictionary result = new Dictionary(); - IDictionary idict = (IDictionary)data; - - foreach (object key in idict.Keys) - { - result.Add(key.ToString(), idict[key]); - } - return result; - } - - /// - /// - protected Dictionary jsonDecode(string json) - { - Dictionary data = new Dictionary(); - data = JsonConvert.DeserializeObject>(json); - data = jsonToDictionary(data); - - return data; - } - - /// - /// - protected static Dictionary jsonToDictionary(Dictionary data) - { - Dictionary result = new Dictionary(); - foreach (KeyValuePair kvp in data) - { - string key = kvp.Key; - object value = kvp.Value; - - if (value.GetType() == typeof(JObject)) - { - Dictionary valueJson = JsonConvert.DeserializeObject>(value.ToString()); - value = jsonToDictionary(valueJson); - } - result.Add(key, value); - } - return result; - } - - /// - /// - protected string httpBuildQuery(Dictionary data) - { - string queryString = null; - foreach (KeyValuePair kvp in data) - { - queryString += kvp.Key + "=" + kvp.Value + "&"; - } - - if (queryString.Length > 0) - { - queryString = queryString.Substring(0, queryString.Length - 1); - } - - return queryString; - } - } -} diff --git a/RetailCrm/RetailCrm.csproj b/RetailCrm/RetailCrm.csproj deleted file mode 100644 index 739db1f..0000000 --- a/RetailCrm/RetailCrm.csproj +++ /dev/null @@ -1,68 +0,0 @@ - - - - - Debug - AnyCPU - {1C407E40-0B79-4593-AC79-03BA8DD76DD1} - Library - Properties - RetailCrm - IntaroCrm - v4.5 - 512 - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - true - - - intaro.pfx - - - - ..\packages\Newtonsoft.Json.6.0.6\lib\net45\Newtonsoft.Json.dll - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/RetailCrm/packages.config b/RetailCrm/packages.config deleted file mode 100644 index 592edee..0000000 --- a/RetailCrm/packages.config +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/lib/RetailCrm.dll b/lib/RetailCrm.dll index 72950aff2f3941394049ad46756491252dca810a..147cbaad32b446904e2fdd9a374d8999ad269582 100644 GIT binary patch literal 19456 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