From eb1e49d8c06eda9ef4ce6df7eb1b2a3a183b19a1 Mon Sep 17 00:00:00 2001 From: Joshua Harms Date: Mon, 3 Oct 2016 10:27:22 -0500 Subject: [PATCH 01/10] Bugfix: should parse 'authorization code was not found or was already used' errors. Closes #70 --- .../Playlists/Exceptions.playlist | 2 +- .../ShopifyExceptionService.cs | 18 +- ...When_receiving_an_oauth_code_used_error.cs | 45 +++++ ShopifySharp.Tests/ShopifySharp.Tests.csproj | 1 + ShopifySharp/Infrastructure/RequestEngine.cs | 158 +++++++++++------- 5 files changed, 159 insertions(+), 65 deletions(-) create mode 100644 ShopifySharp.Tests/ShopifyException Tests/When_receiving_an_oauth_code_used_error.cs diff --git a/ShopifySharp.Tests/Playlists/Exceptions.playlist b/ShopifySharp.Tests/Playlists/Exceptions.playlist index 08871e6c8..add629964 100644 --- a/ShopifySharp.Tests/Playlists/Exceptions.playlist +++ b/ShopifySharp.Tests/Playlists/Exceptions.playlist @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/ShopifySharp.Tests/ShopifyException Tests/ShopifyExceptionService.cs b/ShopifySharp.Tests/ShopifyException Tests/ShopifyExceptionService.cs index b2a508c31..9450773a8 100644 --- a/ShopifySharp.Tests/ShopifyException Tests/ShopifyExceptionService.cs +++ b/ShopifySharp.Tests/ShopifyException Tests/ShopifyExceptionService.cs @@ -1,7 +1,7 @@ using RestSharp; using System; using System.Collections.Generic; -using System.Linq; +using System.Net; using System.Text; using System.Threading.Tasks; @@ -19,6 +19,22 @@ class ShopifyExceptionService : ShopifyService /// An API access token for the shop. public ShopifyExceptionService(string myShopifyUrl, string shopAccessToken): base(myShopifyUrl, shopAccessToken) { } + /// + /// A method that will throw an exception which looks like {"error":"invalid_request","error_description":"The authorization code was not found or was already used"} + /// This error is thrown when trying to authorize an OAuth code that has already been used. + /// + public void ThrowOAuthCodeUsedException() + { + var response = new RestResponse() + { + RawBytes = Encoding.UTF8.GetBytes("{\"error\":\"invalid_request\",\"error_description\":\"The authorization code was not found or was already used\"}"), + StatusCode = HttpStatusCode.NotAcceptable, + StatusDescription = "Not Acceptable" + }; + + RequestEngine.CheckResponseExceptions(response); + } + /// /// A method that will throw an exception which looks like { errors: "some error message"} /// diff --git a/ShopifySharp.Tests/ShopifyException Tests/When_receiving_an_oauth_code_used_error.cs b/ShopifySharp.Tests/ShopifyException Tests/When_receiving_an_oauth_code_used_error.cs new file mode 100644 index 000000000..5157dd2be --- /dev/null +++ b/ShopifySharp.Tests/ShopifyException Tests/When_receiving_an_oauth_code_used_error.cs @@ -0,0 +1,45 @@ +using Machine.Specifications; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ShopifySharp.Tests.ShopifyException_Tests +{ + [Subject(typeof(ShopifyException))] + class When_receiving_an_oauth_code_used_error + { + Establish context = () => + { + + }; + + Because of = () => + { + Ex = Catch.Exception(() => Service.ThrowOAuthCodeUsedException()); + }; + + It should_parse_an_oauth_code_used_error = () => + { + Ex.ShouldBeOfExactType(); + + var e = (ShopifyException)Ex; + + e.Message.ShouldContain("authorization code was not found or was already used"); + e.JsonError.ShouldNotBeNull(); + e.Errors.Count.ShouldEqual(1); + e.Errors.First().Key.ShouldEqual("invalid_request"); + e.Errors.First().Value.Count().ShouldEqual(1); + }; + + Cleanup after = () => + { + + }; + + static ShopifyExceptionService Service = new ShopifyExceptionService(Utils.MyShopifyUrl, Utils.AccessToken); + + static Exception Ex; + } +} diff --git a/ShopifySharp.Tests/ShopifySharp.Tests.csproj b/ShopifySharp.Tests/ShopifySharp.Tests.csproj index cc60c6623..03297261d 100644 --- a/ShopifySharp.Tests/ShopifySharp.Tests.csproj +++ b/ShopifySharp.Tests/ShopifySharp.Tests.csproj @@ -117,6 +117,7 @@ + diff --git a/ShopifySharp/Infrastructure/RequestEngine.cs b/ShopifySharp/Infrastructure/RequestEngine.cs index 6408c12aa..03541e761 100644 --- a/ShopifySharp/Infrastructure/RequestEngine.cs +++ b/ShopifySharp/Infrastructure/RequestEngine.cs @@ -136,76 +136,26 @@ public static void CheckResponseExceptions(IRestResponse response) { if (response.StatusCode != HttpStatusCode.OK && response.StatusCode != HttpStatusCode.Created) { - string json = Encoding.UTF8.GetString(response.RawBytes ?? new byte[] { }); - var errors = new Dictionary>(); + var json = Encoding.UTF8.GetString(response.RawBytes ?? new byte[] { }); + Dictionary> errors = ParseErrorJson(json); - if (string.IsNullOrEmpty(json) == false) + var code = response.StatusCode; + var message = $"Response did not indicate success. Status: {(int)code} {response.StatusDescription}."; + + if (errors == null) { - try + errors = new Dictionary>() { - var parsed = JToken.Parse(string.IsNullOrEmpty(json) ? "{}" : json); - - if (parsed.Any(x => x.Path == "errors")) { - var parsedErrors = parsed["errors"]; - - // Errors can be any of the following: - // 1. { errors: "some error message"} - // 2. { errors: { "order" : "some error message" } } - // 3. { errors: { "order" : [ "some error message" ] } } - - //errors can be either a single string, or an array of other errors - if (parsedErrors.Type == JTokenType.String) - { - //errors is type #1 - - errors.Add("Error", new List() { parsedErrors.Value() }); - } - else - { - //errors is type #2 or #3 - - foreach (var val in parsedErrors.Values()) - { - string name = val.Path.Split('.').Last(); - var list = new List(); - - if (val.Type == JTokenType.String) - { - list.Add(val.Value()); - } - else if (val.Type == JTokenType.Array) - { - list = val.Values().ToList(); - } - - errors.Add(name, list); - } - } - } - } - catch (Exception e) - { - errors.Add(e.Message, new List() { json }); - } - } - - var firstError = errors.FirstOrDefault(); - - // KVPs are structs and can never be null. Instead, check if the firstError equals the default kvp value. - // If so, firstError is null. - var firstErrorIsNull = firstError.Equals(default(KeyValuePair>)); - - HttpStatusCode code = response.StatusCode; - string message = $"Response did not indicate success. Status: {(int)code} {response.StatusDescription}."; - - if (firstErrorIsNull) - { - //Add the generic response message to errors list - errors.Add($"{(int)code} {response.StatusDescription}", new string[] { message }); + $"{(int)code} {response.StatusDescription}", + new string[] { message } + }, + }; } else { + var firstError = errors.First(); + message = $"{firstError.Key}: {string.Join(", ", firstError.Value)}"; } @@ -220,5 +170,87 @@ public static void CheckResponseExceptions(IRestResponse response) throw response.ErrorException; } } + + /// + /// Parses a JSON string for Shopify API errors. + /// + /// Returns null if the JSON could not be parsed into an error. + public static Dictionary> ParseErrorJson(string json) + { + if (string.IsNullOrEmpty(json)) + { + return null; + } + + var errors = new Dictionary>(); + + try + { + var parsed = JToken.Parse(string.IsNullOrEmpty(json) ? "{}" : json); + + // Errors can be any of the following: + // 1. { errors: "some error message"} + // 2. { errors: { "order" : "some error message" } } + // 3. { errors: { "order" : [ "some error message" ] } } + // 4. { error: "invalid_request", error_description:"The authorization code was not found or was already used" } + + if (parsed.Any(p => p.Path == "error") && parsed.Any(p => p.Path == "error_description")) + { + // Error is type #4 + var description = parsed["error_description"]; + + errors.Add("invalid_request", new List() { description.Value() }); + } + else if (parsed.Any(x => x.Path == "errors")) + { + var parsedErrors = parsed["errors"]; + + //errors can be either a single string, or an array of other errors + if (parsedErrors.Type == JTokenType.String) + { + //errors is type #1 + + errors.Add("Error", new List() { parsedErrors.Value() }); + } + else + { + //errors is type #2 or #3 + + foreach (var val in parsedErrors.Values()) + { + string name = val.Path.Split('.').Last(); + var list = new List(); + + if (val.Type == JTokenType.String) + { + list.Add(val.Value()); + } + else if (val.Type == JTokenType.Array) + { + list = val.Values().ToList(); + } + + errors.Add(name, list); + } + } + } + else + { + return null; + } + } + catch (Exception e) + { + errors.Add(e.Message, new List() { json }); + } + + // KVPs are structs and can never be null. Instead, check if the first error equals the default kvp value. + if (errors.FirstOrDefault().Equals(default(KeyValuePair>))) + { + return null; + } + + return errors; + } } } From f907923c9d3a3291593713bdc4d74c74c74f03c8 Mon Sep 17 00:00:00 2001 From: Joshua Harms Date: Mon, 3 Oct 2016 11:14:57 -0500 Subject: [PATCH 02/10] Throw a ShopifyRateLimitException when the rate lmimit is hit. Closes #67. --- .../Playlists/Exceptions.playlist | 2 +- .../When_reaching_the_rate_limit.cs | 79 +++++++++++++++++++ ShopifySharp.Tests/ShopifySharp.Tests.csproj | 1 + ShopifySharp/Infrastructure/RequestEngine.cs | 6 ++ .../ShopifyRateLimitException.cs | 17 ++++ ShopifySharp/ShopifySharp.csproj | 1 + 6 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 ShopifySharp.Tests/ShopifyException Tests/When_reaching_the_rate_limit.cs create mode 100644 ShopifySharp/Infrastructure/ShopifyRateLimitException.cs diff --git a/ShopifySharp.Tests/Playlists/Exceptions.playlist b/ShopifySharp.Tests/Playlists/Exceptions.playlist index add629964..3864fe619 100644 --- a/ShopifySharp.Tests/Playlists/Exceptions.playlist +++ b/ShopifySharp.Tests/Playlists/Exceptions.playlist @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/ShopifySharp.Tests/ShopifyException Tests/When_reaching_the_rate_limit.cs b/ShopifySharp.Tests/ShopifyException Tests/When_reaching_the_rate_limit.cs new file mode 100644 index 000000000..79c583225 --- /dev/null +++ b/ShopifySharp.Tests/ShopifyException Tests/When_reaching_the_rate_limit.cs @@ -0,0 +1,79 @@ +using Machine.Specifications; +using ShopifySharp.Filters; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ShopifySharp.Tests.ShopifyException_Tests +{ + [Subject(typeof(ShopifyRateLimitException))] + class When_reaching_the_rate_limit + { + Establish context = () => + { + + }; + + Because of = () => + { + try + { + throw new ShopifyRateLimitException(); + } + catch (ShopifyException e) + { + CaughtRateExceptionWithBaseException = true; + } + + var tasks = new List>>(); + + // Reach the rate limit of 40 requests per second + for (int i = 0; i < 100; i++) + { + tasks.Add(Service.ListAsync(new ShopifyEventListFilter() + { + Limit = 1, + })); + } + + try + { + var result = Task.WhenAll(tasks.ToArray()).Await().AsTask.Result; + } + catch (AggregateException e) + { + Ex = e.InnerException; + } + catch (Exception e) + { + Ex = e; + } + }; + + It should_throw_a_rate_limit_exception = () => + { + CaughtRateExceptionWithBaseException.ShouldBeTrue(); + Ex.ShouldBeOfExactType(); + + var e = (ShopifyException)Ex; + + e.JsonError.ShouldNotBeNull(); + e.Errors.Count.ShouldBeGreaterThanOrEqualTo(1); + e.Errors.First().Key.Equals("Error").ShouldBeTrue(); + e.Errors.First().Value.First().ShouldEqual("Exceeded 2 calls per second for api client. Reduce request rates to resume uninterrupted service."); + }; + + Cleanup after = () => + { + + }; + + static ShopifyEventService Service = new ShopifyEventService(Utils.MyShopifyUrl, Utils.AccessToken); + + static Exception Ex; + + static bool CaughtRateExceptionWithBaseException = false; + } +} diff --git a/ShopifySharp.Tests/ShopifySharp.Tests.csproj b/ShopifySharp.Tests/ShopifySharp.Tests.csproj index 03297261d..6111f26cc 100644 --- a/ShopifySharp.Tests/ShopifySharp.Tests.csproj +++ b/ShopifySharp.Tests/ShopifySharp.Tests.csproj @@ -114,6 +114,7 @@ + diff --git a/ShopifySharp/Infrastructure/RequestEngine.cs b/ShopifySharp/Infrastructure/RequestEngine.cs index 03541e761..5b19ca7bc 100644 --- a/ShopifySharp/Infrastructure/RequestEngine.cs +++ b/ShopifySharp/Infrastructure/RequestEngine.cs @@ -158,6 +158,12 @@ public static void CheckResponseExceptions(IRestResponse response) message = $"{firstError.Key}: {string.Join(", ", firstError.Value)}"; } + + // If the error was caused by reaching the API rate limit, throw a rate limit exception. + if ((int) code == 429 /* Too many requests */) + { + throw new ShopifyRateLimitException(code, errors, message, json); + } throw new ShopifyException(code, errors, message, json); } diff --git a/ShopifySharp/Infrastructure/ShopifyRateLimitException.cs b/ShopifySharp/Infrastructure/ShopifyRateLimitException.cs new file mode 100644 index 000000000..4cd4bbecd --- /dev/null +++ b/ShopifySharp/Infrastructure/ShopifyRateLimitException.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Net; + +namespace ShopifySharp +{ + /// + /// An exception thrown when an API call has reached Shopify's rate limit. + /// + public class ShopifyRateLimitException : ShopifyException + { + public ShopifyRateLimitException() : base() { } + + public ShopifyRateLimitException(string message): base(message) { } + + public ShopifyRateLimitException(HttpStatusCode httpStatusCode, Dictionary> errors, string message, string jsonError) : base(httpStatusCode, errors, message, jsonError) { } + } +} diff --git a/ShopifySharp/ShopifySharp.csproj b/ShopifySharp/ShopifySharp.csproj index c041b263c..633af3ad8 100644 --- a/ShopifySharp/ShopifySharp.csproj +++ b/ShopifySharp/ShopifySharp.csproj @@ -91,6 +91,7 @@ + From 823804386ca08438566afb5065f6ddcb08ee6c23 Mon Sep 17 00:00:00 2001 From: Joshua Harms Date: Mon, 3 Oct 2016 15:30:38 -0500 Subject: [PATCH 03/10] Add support for Order Risks Closes #71. --- .../Playlists/OrderRisks.playlist | 1 + .../ShopifyOrderRiskService Tests/Utils.cs | 45 ++++++++++ .../When_creating_a_risk.cs | 44 ++++++++++ .../When_deleting_a_risk.cs | 37 +++++++++ .../When_getting_a_risk.cs | 47 +++++++++++ .../When_listing_order_risks.cs | 44 ++++++++++ .../When_updating_a_risk.cs | 41 ++++++++++ ShopifySharp.Tests/ShopifySharp.Tests.csproj | 6 ++ ShopifySharp/Entities/ShopifyOrderRisk.cs | 66 +++++++++++++++ .../Services/OrderRisk/ShopifyOrderService.cs | 82 +++++++++++++++++++ ShopifySharp/ShopifySharp.csproj | 2 + 11 files changed, 415 insertions(+) create mode 100644 ShopifySharp.Tests/Playlists/OrderRisks.playlist create mode 100644 ShopifySharp.Tests/ShopifyOrderRiskService Tests/Utils.cs create mode 100644 ShopifySharp.Tests/ShopifyOrderRiskService Tests/When_creating_a_risk.cs create mode 100644 ShopifySharp.Tests/ShopifyOrderRiskService Tests/When_deleting_a_risk.cs create mode 100644 ShopifySharp.Tests/ShopifyOrderRiskService Tests/When_getting_a_risk.cs create mode 100644 ShopifySharp.Tests/ShopifyOrderRiskService Tests/When_listing_order_risks.cs create mode 100644 ShopifySharp.Tests/ShopifyOrderRiskService Tests/When_updating_a_risk.cs create mode 100644 ShopifySharp/Entities/ShopifyOrderRisk.cs create mode 100644 ShopifySharp/Services/OrderRisk/ShopifyOrderService.cs diff --git a/ShopifySharp.Tests/Playlists/OrderRisks.playlist b/ShopifySharp.Tests/Playlists/OrderRisks.playlist new file mode 100644 index 000000000..12d7ec2ec --- /dev/null +++ b/ShopifySharp.Tests/Playlists/OrderRisks.playlist @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ShopifySharp.Tests/ShopifyOrderRiskService Tests/Utils.cs b/ShopifySharp.Tests/ShopifyOrderRiskService Tests/Utils.cs new file mode 100644 index 000000000..3bb758bab --- /dev/null +++ b/ShopifySharp.Tests/ShopifyOrderRiskService Tests/Utils.cs @@ -0,0 +1,45 @@ +using System.Linq; +using System.Threading.Tasks; + +namespace ShopifySharp.Tests.ShopifyOrderRiskService_Tests +{ + static class RiskUtils + { + public static string Message = "This looks risky!"; + + public static decimal Score = (decimal)0.85; + + public static string Recommendation = "cancel"; + + public static string Source = "External"; + + public static bool CauseCancel = false; + + public static bool Display = true; + + public static ShopifyOrderRisk CreateRisk() + { + return new ShopifyOrderRisk() + { + Message = Message, + Score = Score, + Recommendation = Recommendation, + Source = Source, + CauseCancel = CauseCancel, + Display = Display, + }; + } + + public static async Task GetOrderId() + { + var service = new ShopifyOrderService(Utils.MyShopifyUrl, Utils.AccessToken); + var orders = await service.ListAsync(new Filters.ShopifyOrderFilter() + { + Limit = 1, + Fields = "id" + }); + + return orders.First().Id.Value; + } + } +} diff --git a/ShopifySharp.Tests/ShopifyOrderRiskService Tests/When_creating_a_risk.cs b/ShopifySharp.Tests/ShopifyOrderRiskService Tests/When_creating_a_risk.cs new file mode 100644 index 000000000..91a348dad --- /dev/null +++ b/ShopifySharp.Tests/ShopifyOrderRiskService Tests/When_creating_a_risk.cs @@ -0,0 +1,44 @@ +using Machine.Specifications; + +namespace ShopifySharp.Tests.ShopifyOrderRiskService_Tests +{ + [Subject(typeof(ShopifyOrderRiskService))] + class When_creating_a_risk + { + Establish context = () => + { + OrderId = RiskUtils.GetOrderId().Await().AsTask.Result; + }; + + Because of = () => + { + Risk = Service.CreateAsync(OrderId, RiskUtils.CreateRisk()).Await().AsTask.Result; + }; + + It should_create_a_risk = () => + { + Risk.ShouldNotBeNull(); + Risk.OrderId.ShouldEqual(OrderId); + Risk.Message.ShouldEqual(RiskUtils.Message); + Risk.Score.ShouldEqual(RiskUtils.Score); + Risk.Recommendation.ShouldEqual(RiskUtils.Recommendation); + Risk.Source.ShouldEqual(RiskUtils.Source); + Risk.CauseCancel.ShouldEqual(RiskUtils.CauseCancel); + Risk.Display.ShouldEqual(RiskUtils.Display); + }; + + Cleanup after = () => + { + if (Risk != null) + { + Service.DeleteAsync(OrderId, Risk.Id.Value).Await(); + } + }; + + static long OrderId; + + static ShopifyOrderRisk Risk; + + static ShopifyOrderRiskService Service = new ShopifyOrderRiskService(Utils.MyShopifyUrl, Utils.AccessToken); + } +} diff --git a/ShopifySharp.Tests/ShopifyOrderRiskService Tests/When_deleting_a_risk.cs b/ShopifySharp.Tests/ShopifyOrderRiskService Tests/When_deleting_a_risk.cs new file mode 100644 index 000000000..018983095 --- /dev/null +++ b/ShopifySharp.Tests/ShopifyOrderRiskService Tests/When_deleting_a_risk.cs @@ -0,0 +1,37 @@ +using Machine.Specifications; +using System; + +namespace ShopifySharp.Tests.ShopifyOrderRiskService_Tests +{ + [Subject(typeof(ShopifyOrderRiskService))] + class When_deleting_a_risk + { + Establish context = () => + { + OrderId = RiskUtils.GetOrderId().Await().AsTask.Result; + RiskId = Service.CreateAsync(OrderId, RiskUtils.CreateRisk()).Await().AsTask.Result.Id.Value; + }; + + Because of = () => + { + Ex = Catch.Exception(() => Service.DeleteAsync(OrderId, RiskId).Await()); + }; + + It should_delete_a_risk = () => + { + Ex.ShouldBeNull(); + }; + + Cleanup after = () => + { + }; + + static long OrderId; + + static long RiskId; + + static Exception Ex; + + static ShopifyOrderRiskService Service = new ShopifyOrderRiskService(Utils.MyShopifyUrl, Utils.AccessToken); + } +} diff --git a/ShopifySharp.Tests/ShopifyOrderRiskService Tests/When_getting_a_risk.cs b/ShopifySharp.Tests/ShopifyOrderRiskService Tests/When_getting_a_risk.cs new file mode 100644 index 000000000..2e1538dab --- /dev/null +++ b/ShopifySharp.Tests/ShopifyOrderRiskService Tests/When_getting_a_risk.cs @@ -0,0 +1,47 @@ +using Machine.Specifications; + +namespace ShopifySharp.Tests.ShopifyOrderRiskService_Tests +{ + [Subject(typeof(ShopifyOrderRiskService))] + class When_getting_a_risk + { + Establish context = () => + { + OrderId = RiskUtils.GetOrderId().Await().AsTask.Result; + RiskId = Service.CreateAsync(OrderId, RiskUtils.CreateRisk()).Await().AsTask.Result.Id.Value; + }; + + Because of = () => + { + Risk = Service.GetAsync(OrderId, RiskId).Await().AsTask.Result; + }; + + It should_get_a_risk = () => + { + Risk.ShouldNotBeNull(); + Risk.OrderId.ShouldEqual(OrderId); + Risk.Message.ShouldEqual(RiskUtils.Message); + Risk.Score.ShouldEqual(RiskUtils.Score); + Risk.Recommendation.ShouldEqual(RiskUtils.Recommendation); + Risk.Source.ShouldEqual(RiskUtils.Source); + Risk.CauseCancel.ShouldEqual(RiskUtils.CauseCancel); + Risk.Display.ShouldEqual(RiskUtils.Display); + }; + + Cleanup after = () => + { + if (Risk != null) + { + Service.DeleteAsync(OrderId, Risk.Id.Value).Await(); + } + }; + + static long RiskId; + + static long OrderId; + + static ShopifyOrderRisk Risk; + + static ShopifyOrderRiskService Service = new ShopifyOrderRiskService(Utils.MyShopifyUrl, Utils.AccessToken); + } +} diff --git a/ShopifySharp.Tests/ShopifyOrderRiskService Tests/When_listing_order_risks.cs b/ShopifySharp.Tests/ShopifyOrderRiskService Tests/When_listing_order_risks.cs new file mode 100644 index 000000000..4248b4f1b --- /dev/null +++ b/ShopifySharp.Tests/ShopifyOrderRiskService Tests/When_listing_order_risks.cs @@ -0,0 +1,44 @@ +using Machine.Specifications; +using System.Collections.Generic; +using System.Linq; + +namespace ShopifySharp.Tests.ShopifyOrderRiskService_Tests +{ + [Subject(typeof(ShopifyOrderRiskService))] + class When_listing_order_risks + { + Establish context = () => + { + OrderId = RiskUtils.GetOrderId().Await().AsTask.Result; + }; + + Because of = () => + { + Risks = Service.ListAsync(OrderId).Await().AsTask.Result; + }; + + It should_list_order_risks = () => + { + Risks.ShouldNotBeNull(); + + // Not all orders have a risk + if (Risks.Count() >= 1) + { + Risks.All(r => r.OrderId.HasValue).ShouldBeTrue(); + Risks.All(r => string.IsNullOrEmpty(r.Message) == false).ShouldBeTrue(); + Risks.All(r => r.Id.HasValue).ShouldBeTrue(); + } + }; + + Cleanup after = () => + { + + }; + + static long OrderId { get; set; } + + static ShopifyOrderRiskService Service = new ShopifyOrderRiskService(Utils.MyShopifyUrl, Utils.AccessToken); + + static IEnumerable Risks; + } +} diff --git a/ShopifySharp.Tests/ShopifyOrderRiskService Tests/When_updating_a_risk.cs b/ShopifySharp.Tests/ShopifyOrderRiskService Tests/When_updating_a_risk.cs new file mode 100644 index 000000000..c67971292 --- /dev/null +++ b/ShopifySharp.Tests/ShopifyOrderRiskService Tests/When_updating_a_risk.cs @@ -0,0 +1,41 @@ +using Machine.Specifications; + +namespace ShopifySharp.Tests.ShopifyOrderRiskService_Tests +{ + [Subject(typeof(ShopifyOrderRiskService))] + class When_updating_a_risk + { + Establish context = () => + { + OrderId = RiskUtils.GetOrderId().Await().AsTask.Result; + Risk = Service.CreateAsync(OrderId, RiskUtils.CreateRisk()).Await().AsTask.Result; + }; + + Because of = () => + { + Risk.Message = UpdatedMessage; + Risk = Service.UpdateAsync(OrderId, Risk).Await().AsTask.Result; + }; + + It should_update_a_risk = () => + { + Risk.Message.ShouldEqual(UpdatedMessage); + }; + + Cleanup after = () => + { + if (Risk != null) + { + Service.DeleteAsync(OrderId, Risk.Id.Value).Await(); + } + }; + + static long OrderId; + + static ShopifyOrderRisk Risk; + + static string UpdatedMessage = "An updated risk message."; + + static ShopifyOrderRiskService Service = new ShopifyOrderRiskService(Utils.MyShopifyUrl, Utils.AccessToken); + } +} diff --git a/ShopifySharp.Tests/ShopifySharp.Tests.csproj b/ShopifySharp.Tests/ShopifySharp.Tests.csproj index 6111f26cc..feeca3817 100644 --- a/ShopifySharp.Tests/ShopifySharp.Tests.csproj +++ b/ShopifySharp.Tests/ShopifySharp.Tests.csproj @@ -142,6 +142,12 @@ + + + + + + diff --git a/ShopifySharp/Entities/ShopifyOrderRisk.cs b/ShopifySharp/Entities/ShopifyOrderRisk.cs new file mode 100644 index 000000000..742e8a31e --- /dev/null +++ b/ShopifySharp/Entities/ShopifyOrderRisk.cs @@ -0,0 +1,66 @@ +using Newtonsoft.Json; +using System; + +namespace ShopifySharp +{ + /// + /// An object representing a Shopify order risk. + /// + public class ShopifyOrderRisk : ShopifyObject + { + /// + /// Use this flag when a fraud check is accompanied with a call to the Orders API to cancel the order. This will indicate to the merchant that this risk was severe enough to force cancellation of the order. + /// Note: Setting this parameter does not cancel the order. This must be done by the Orders API. + /// + [JsonProperty("cause_cancel")] + public bool CauseCancel { get; set; } + + /// + /// WARNING: This is an undocumented value returned by the Shopify API. Use at your own risk. + /// + [JsonProperty("checkout_id"), Obsolete("This is an undocumented value returned by the Shopify API. Use at your own risk.")] + public long? CheckoutId { get; set; } + + /// + /// States whether or not the risk is displayed. Valid values are "true" or "false". + /// + [JsonProperty("display")] + public bool Display { get; set; } + + /// + /// The id of the order the order risk belongs to + /// + [JsonProperty("order_id")] + public long? OrderId { get; set; } + + /// + /// A message that should be displayed to the merchant to indicate the results of the fraud check. + /// + [JsonProperty("message")] + public string Message { get; set; } + + /// + /// WARNING: This is an undocumented field returned by the Shopify API. Use at your own risk. + /// + [JsonProperty("merchant_message"), Obsolete("This is an undocumented field returned by the Shopify API. Use at your own risk.")] + public string MerchantMessage { get; set; } + + /// + /// The recommended action given to the merchant. Known values are 'cancel', 'investigate' and 'accept'. + /// + [JsonProperty("recommendation")] + public string Recommendation { get; set; } + + /// + /// A number between 0 and 1 indicating percentage likelihood of being fraud. + /// + [JsonProperty("score")] + public decimal Score { get; set; } + + /// + /// This indicates the source of the risk assessment. Only known value is 'External'. + /// + [JsonProperty("source")] + public string Source { get; set; } + } +} diff --git a/ShopifySharp/Services/OrderRisk/ShopifyOrderService.cs b/ShopifySharp/Services/OrderRisk/ShopifyOrderService.cs new file mode 100644 index 000000000..4bc1d9afb --- /dev/null +++ b/ShopifySharp/Services/OrderRisk/ShopifyOrderService.cs @@ -0,0 +1,82 @@ +using RestSharp; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace ShopifySharp +{ + /// + /// A service for manipulating Shopify order risks. + /// + public class ShopifyOrderRiskService : ShopifyService + { + /// + /// Creates a new instance of . + /// + /// The shop's *.myshopify.com URL. + /// An API access token for the shop. + public ShopifyOrderRiskService(string myShopifyUrl, string shopAccessToken) : base(myShopifyUrl, shopAccessToken) { } + + /// + /// Gets a list of all order risks for an order. + /// + /// The order the risks belong to. + public async Task> ListAsync(long orderId) + { + var req = RequestEngine.CreateRequest($"orders/{orderId}/risks.json", Method.GET, "risks"); + + return await RequestEngine.ExecuteRequestAsync>(_RestClient, req); + } + + /// + /// Retrieves the with the given id. + /// + /// The order the risk belongs to. + /// The id of the risk to retrieve. + public async Task GetAsync(long orderId, long riskId) + { + var req = RequestEngine.CreateRequest($"orders/{orderId}/risks/{riskId}.json", Method.GET, "risk"); + + return await RequestEngine.ExecuteRequestAsync(_RestClient, req); + } + + /// + /// Creates a new on the order. + /// + /// The order the risk belongs to. + /// A new . Id should be set to null. + public async Task CreateAsync(long orderId, ShopifyOrderRisk risk) + { + var req = RequestEngine.CreateRequest($"orders/{orderId}/risks.json", Method.POST, "risk"); + + req.AddJsonBody(new { risk }); + + return await RequestEngine.ExecuteRequestAsync(_RestClient, req); + } + + /// + /// Updates the given . Id must not be null. + /// + /// The order the risk belongs to. + /// The risk to update. + public async Task UpdateAsync(long orderId, ShopifyOrderRisk risk) + { + var req = RequestEngine.CreateRequest($"orders/{orderId}/risks/{risk.Id.Value}.json", Method.PUT, "risk"); + + req.AddJsonBody(new { risk }); + + return await RequestEngine.ExecuteRequestAsync(_RestClient, req); + } + + /// + /// Deletes an order with the given Id. + /// + /// The order the risk belongs to. + /// The risk's id. + public async Task DeleteAsync(long orderId, long riskId) + { + var req = RequestEngine.CreateRequest($"orders/{orderId}/risks/{riskId}.json", Method.DELETE); + + await RequestEngine.ExecuteRequestAsync(_RestClient, req); + } + } +} diff --git a/ShopifySharp/ShopifySharp.csproj b/ShopifySharp/ShopifySharp.csproj index 633af3ad8..981c572d8 100644 --- a/ShopifySharp/ShopifySharp.csproj +++ b/ShopifySharp/ShopifySharp.csproj @@ -65,6 +65,7 @@ + @@ -112,6 +113,7 @@ + From 90591a184a4c374a43931a4a56044ea98e72b70a Mon Sep 17 00:00:00 2001 From: Joshua Harms Date: Mon, 3 Oct 2016 16:30:45 -0500 Subject: [PATCH 04/10] Add 'Internal' and 'Gateway' to known source values. --- ShopifySharp/Entities/ShopifyOrderRisk.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ShopifySharp/Entities/ShopifyOrderRisk.cs b/ShopifySharp/Entities/ShopifyOrderRisk.cs index 742e8a31e..469e6d075 100644 --- a/ShopifySharp/Entities/ShopifyOrderRisk.cs +++ b/ShopifySharp/Entities/ShopifyOrderRisk.cs @@ -40,7 +40,7 @@ public class ShopifyOrderRisk : ShopifyObject public string Message { get; set; } /// - /// WARNING: This is an undocumented field returned by the Shopify API. Use at your own risk. + /// WARNING: This is an undocumented field returned by the Shopify API. Use at your own risk. This value cannot be set via API. This message is shown in the merchant's admin dashboard if different from . /// [JsonProperty("merchant_message"), Obsolete("This is an undocumented field returned by the Shopify API. Use at your own risk.")] public string MerchantMessage { get; set; } @@ -58,7 +58,7 @@ public class ShopifyOrderRisk : ShopifyObject public decimal Score { get; set; } /// - /// This indicates the source of the risk assessment. Only known value is 'External'. + /// This indicates the source of the risk assessment. Known values are 'External', 'Internal' and 'Gateway'. /// [JsonProperty("source")] public string Source { get; set; } From 0b47677d32ece7a6a955205b30ecdd4b09814aba Mon Sep 17 00:00:00 2001 From: Joshua Harms Date: Tue, 4 Oct 2016 15:14:11 -0500 Subject: [PATCH 05/10] Rename OrderRiskService file --- .../{ShopifyOrderService.cs => ShopifyOrderRiskService.cs} | 0 ShopifySharp/ShopifySharp.csproj | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename ShopifySharp/Services/OrderRisk/{ShopifyOrderService.cs => ShopifyOrderRiskService.cs} (100%) diff --git a/ShopifySharp/Services/OrderRisk/ShopifyOrderService.cs b/ShopifySharp/Services/OrderRisk/ShopifyOrderRiskService.cs similarity index 100% rename from ShopifySharp/Services/OrderRisk/ShopifyOrderService.cs rename to ShopifySharp/Services/OrderRisk/ShopifyOrderRiskService.cs diff --git a/ShopifySharp/ShopifySharp.csproj b/ShopifySharp/ShopifySharp.csproj index 981c572d8..616da2204 100644 --- a/ShopifySharp/ShopifySharp.csproj +++ b/ShopifySharp/ShopifySharp.csproj @@ -113,7 +113,7 @@ - + From 521d059fd9088064c80c939929129db6ab7c8d48 Mon Sep 17 00:00:00 2001 From: Joshua Harms Date: Tue, 4 Oct 2016 15:23:23 -0500 Subject: [PATCH 06/10] Bugfix: Opening and closing an order should return the order. Closes #76 --- .../When_closing_an_order.cs | 14 ++++++++------ .../When_opening_an_order.cs | 16 +++++++++------- .../Services/Order/ShopifyOrderService.cs | 12 ++++++------ 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/ShopifySharp.Tests/ShopifyOrderService Tests/When_closing_an_order.cs b/ShopifySharp.Tests/ShopifyOrderService Tests/When_closing_an_order.cs index 8d52b0f48..b88811454 100644 --- a/ShopifySharp.Tests/ShopifyOrderService Tests/When_closing_an_order.cs +++ b/ShopifySharp.Tests/ShopifyOrderService Tests/When_closing_an_order.cs @@ -14,28 +14,30 @@ public class When_closing_an_order Establish context = () => { Service = new ShopifyOrderService(Utils.MyShopifyUrl, Utils.AccessToken); - Order = Service.CreateAsync(OrderCreation.GenerateOrder()).Await().AsTask.Result; + Id = Service.CreateAsync(OrderCreation.GenerateOrder()).Await().AsTask.Result.Id.Value; }; Because of = () => { - Service.CloseAsync(Order.Id.Value).Await(); + Order = Service.CloseAsync(Id).Await().AsTask.Result; }; It should_close_an_order = () => { - //Update the order from Shopify - Order = Service.GetAsync(Order.Id.Value).Await().AsTask.Result; - + Order.ShouldNotBeNull(); + Order.Id.ShouldEqual(Id); Order.ClosedAt.HasValue.ShouldBeTrue(); }; Cleanup after = () => { - Service.DeleteAsync(Order.Id.Value).Await(); + Service.DeleteAsync(Id).Await(); }; static ShopifyOrderService Service; + static ShopifyOrder Order; + + static long Id; } } diff --git a/ShopifySharp.Tests/ShopifyOrderService Tests/When_opening_an_order.cs b/ShopifySharp.Tests/ShopifyOrderService Tests/When_opening_an_order.cs index 13d70f7f4..8faa7ba4d 100644 --- a/ShopifySharp.Tests/ShopifyOrderService Tests/When_opening_an_order.cs +++ b/ShopifySharp.Tests/ShopifyOrderService Tests/When_opening_an_order.cs @@ -14,29 +14,31 @@ public class When_opening_an_order Establish context = () => { Service = new ShopifyOrderService(Utils.MyShopifyUrl, Utils.AccessToken); - Order = Service.CreateAsync(OrderCreation.GenerateOrder()).Await().AsTask.Result; + Id = Service.CreateAsync(OrderCreation.GenerateOrder()).Await().AsTask.Result.Id.Value; + Service.CloseAsync(Id).Await(); }; Because of = () => { - Service.CloseAsync(Order.Id.Value).Await(); - Service.OpenAsync(Order.Id.Value).Await(); + Order = Service.OpenAsync(Id).Await().AsTask.Result; }; It should_open_an_order = () => { - //Update the order from Shopify - Order = Service.GetAsync(Order.Id.Value).Await().AsTask.Result; - + Order.ShouldNotBeNull(); + Order.Id.ShouldEqual(Id); Order.ClosedAt.HasValue.ShouldBeFalse(); }; Cleanup after = () => { - Service.DeleteAsync(Order.Id.Value).Await(); + Service.DeleteAsync(Id).Await(); }; static ShopifyOrderService Service; + static ShopifyOrder Order; + + static long Id; } } diff --git a/ShopifySharp/Services/Order/ShopifyOrderService.cs b/ShopifySharp/Services/Order/ShopifyOrderService.cs index 3a79cae0a..656c13216 100644 --- a/ShopifySharp/Services/Order/ShopifyOrderService.cs +++ b/ShopifySharp/Services/Order/ShopifyOrderService.cs @@ -101,22 +101,22 @@ public async Task GetAsync(long orderId, string fields = null) /// Closes an order. /// /// The order's id. - public async Task CloseAsync(long id) + public async Task CloseAsync(long id) { - IRestRequest req = RequestEngine.CreateRequest($"orders/{id}/close.json", Method.POST); + var req = RequestEngine.CreateRequest($"orders/{id}/close.json", Method.POST, "order"); - await RequestEngine.ExecuteRequestAsync(_RestClient, req); + return await RequestEngine.ExecuteRequestAsync(_RestClient, req); } /// /// Opens a closed order. /// /// The order's id. - public async Task OpenAsync(long id) + public async Task OpenAsync(long id) { - IRestRequest req = RequestEngine.CreateRequest($"orders/{id}/open.json", Method.POST); + var req = RequestEngine.CreateRequest($"orders/{id}/open.json", Method.POST, "order"); - await RequestEngine.ExecuteRequestAsync(_RestClient, req); + return await RequestEngine.ExecuteRequestAsync(_RestClient, req); } /// From cd76aa96b8d177b601d0532f1e78c156f1255133 Mon Sep 17 00:00:00 2001 From: Joshua Harms Date: Thu, 6 Oct 2016 14:14:03 -0500 Subject: [PATCH 07/10] Add ShopifySmartCollectionService Closes #72 --- .../Playlists/SmartCollections.playlist | 1 + ShopifySharp.Tests/ShopifySharp.Tests.csproj | 7 ++ .../Utils.cs | 21 ++++ .../When_counting_smart_collections.cs | 32 ++++++ .../When_creating_a_smart_collection.cs | 39 +++++++ .../When_deleting_a_smart_collection.cs | 35 ++++++ .../When_getting_a_collection.cs | 41 +++++++ .../When_listing_smart_collections.cs | 50 +++++++++ .../When_updating_a_smart_collection.cs | 42 ++++++++ .../Entities/ShopifySmartCollection.cs | 12 +-- .../Entities/ShopifySmartCollectionImage.cs | 29 +++++ .../Entities/ShopifySmartCollectionRules.cs | 6 -- .../Filters/ShopifySmartCollectionFilter.cs | 28 +++++ .../ShopifySmartCollectionService.cs | 100 ++++++++++++++++++ ShopifySharp/ShopifySharp.csproj | 3 + readme.md | 62 +++++++++++ 16 files changed, 494 insertions(+), 14 deletions(-) create mode 100644 ShopifySharp.Tests/Playlists/SmartCollections.playlist create mode 100644 ShopifySharp.Tests/ShopifySmartCollectionService Tests/Utils.cs create mode 100644 ShopifySharp.Tests/ShopifySmartCollectionService Tests/When_counting_smart_collections.cs create mode 100644 ShopifySharp.Tests/ShopifySmartCollectionService Tests/When_creating_a_smart_collection.cs create mode 100644 ShopifySharp.Tests/ShopifySmartCollectionService Tests/When_deleting_a_smart_collection.cs create mode 100644 ShopifySharp.Tests/ShopifySmartCollectionService Tests/When_getting_a_collection.cs create mode 100644 ShopifySharp.Tests/ShopifySmartCollectionService Tests/When_listing_smart_collections.cs create mode 100644 ShopifySharp.Tests/ShopifySmartCollectionService Tests/When_updating_a_smart_collection.cs create mode 100644 ShopifySharp/Entities/ShopifySmartCollectionImage.cs create mode 100644 ShopifySharp/Filters/ShopifySmartCollectionFilter.cs create mode 100644 ShopifySharp/Services/SmartCollection/ShopifySmartCollectionService.cs diff --git a/ShopifySharp.Tests/Playlists/SmartCollections.playlist b/ShopifySharp.Tests/Playlists/SmartCollections.playlist new file mode 100644 index 000000000..10296285d --- /dev/null +++ b/ShopifySharp.Tests/Playlists/SmartCollections.playlist @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ShopifySharp.Tests/ShopifySharp.Tests.csproj b/ShopifySharp.Tests/ShopifySharp.Tests.csproj index feeca3817..747e9eb38 100644 --- a/ShopifySharp.Tests/ShopifySharp.Tests.csproj +++ b/ShopifySharp.Tests/ShopifySharp.Tests.csproj @@ -218,6 +218,13 @@ + + + + + + + diff --git a/ShopifySharp.Tests/ShopifySmartCollectionService Tests/Utils.cs b/ShopifySharp.Tests/ShopifySmartCollectionService Tests/Utils.cs new file mode 100644 index 000000000..fc04181ac --- /dev/null +++ b/ShopifySharp.Tests/ShopifySmartCollectionService Tests/Utils.cs @@ -0,0 +1,21 @@ +namespace ShopifySharp.Tests.ShopifySmartCollectionService_Tests +{ + static class SmartCollectionUtils + { + public static string BodyHtml { get; } = "

Hello world!

"; + + public static string Handle { get; } = "ShopifySharp-Handle"; + + public static string Title { get; } = "ShopifySharp Test Smart Collection"; + + public static ShopifySmartCollection CreateCollection() + { + return new ShopifySmartCollection() + { + BodyHtml = BodyHtml, + Handle = Handle, + Title = Title, + }; + } + } +} diff --git a/ShopifySharp.Tests/ShopifySmartCollectionService Tests/When_counting_smart_collections.cs b/ShopifySharp.Tests/ShopifySmartCollectionService Tests/When_counting_smart_collections.cs new file mode 100644 index 000000000..1fe7b0ae1 --- /dev/null +++ b/ShopifySharp.Tests/ShopifySmartCollectionService Tests/When_counting_smart_collections.cs @@ -0,0 +1,32 @@ +using Machine.Specifications; + +namespace ShopifySharp.Tests.ShopifySmartCollectionService_Tests +{ + [Subject(typeof(ShopifySmartCollectionService))] + class When_counting_smart_collections + { + Establish context = () => + { + + }; + + Because of = () => + { + Count = Service.CountAsync().Await(); + }; + + It should_count_smart_collections = () => + { + Count.HasValue.ShouldBeTrue(); + }; + + Cleanup after = () => + { + + }; + + static ShopifySmartCollectionService Service = new ShopifySmartCollectionService(Utils.MyShopifyUrl, Utils.AccessToken); + + static int? Count; + } +} diff --git a/ShopifySharp.Tests/ShopifySmartCollectionService Tests/When_creating_a_smart_collection.cs b/ShopifySharp.Tests/ShopifySmartCollectionService Tests/When_creating_a_smart_collection.cs new file mode 100644 index 000000000..674d8963e --- /dev/null +++ b/ShopifySharp.Tests/ShopifySmartCollectionService Tests/When_creating_a_smart_collection.cs @@ -0,0 +1,39 @@ +using Machine.Specifications; + +namespace ShopifySharp.Tests.ShopifySmartCollectionService_Tests +{ + [Subject(typeof(ShopifySmartCollectionService))] + class When_creating_a_smart_collection + { + Establish context = () => + { + + }; + + Because of = () => + { + Collection = Service.CreateAsync(SmartCollectionUtils.CreateCollection()).Await(); + }; + + It should_create_a_smart_collection = () => + { + Collection.ShouldNotBeNull(); + Collection.Id.HasValue.ShouldBeTrue(); + Collection.BodyHtml.ShouldEqual(SmartCollectionUtils.BodyHtml); + Collection.Handle.ShouldBeEqualIgnoringCase(SmartCollectionUtils.Handle); + Collection.Title.ShouldEqual(SmartCollectionUtils.Title); + }; + + Cleanup after = () => + { + if (Collection != null && Collection.Id.HasValue) + { + Service.DeleteAsync(Collection.Id.Value).Await(); + } + }; + + static ShopifySmartCollectionService Service = new ShopifySmartCollectionService(Utils.MyShopifyUrl, Utils.AccessToken); + + static ShopifySmartCollection Collection; + } +} diff --git a/ShopifySharp.Tests/ShopifySmartCollectionService Tests/When_deleting_a_smart_collection.cs b/ShopifySharp.Tests/ShopifySmartCollectionService Tests/When_deleting_a_smart_collection.cs new file mode 100644 index 000000000..1731949a9 --- /dev/null +++ b/ShopifySharp.Tests/ShopifySmartCollectionService Tests/When_deleting_a_smart_collection.cs @@ -0,0 +1,35 @@ +using Machine.Specifications; +using System; + +namespace ShopifySharp.Tests.ShopifySmartCollectionService_Tests +{ + [Subject(typeof(ShopifySmartCollectionService))] + class When_deleting_a_smart_collection + { + Establish context = () => + { + CollectionId = Service.CreateAsync(SmartCollectionUtils.CreateCollection()).Await().AsTask.Result.Id; + }; + + Because of = () => + { + Ex = Catch.Exception(() => Service.DeleteAsync(CollectionId.Value).Await()); + }; + + It should_delete_a_smart_collection = () => + { + Ex.ShouldBeNull(); + }; + + Cleanup after = () => + { + + }; + + static ShopifySmartCollectionService Service = new ShopifySmartCollectionService(Utils.MyShopifyUrl, Utils.AccessToken); + + static long? CollectionId; + + static Exception Ex; + } +} diff --git a/ShopifySharp.Tests/ShopifySmartCollectionService Tests/When_getting_a_collection.cs b/ShopifySharp.Tests/ShopifySmartCollectionService Tests/When_getting_a_collection.cs new file mode 100644 index 000000000..628a39de6 --- /dev/null +++ b/ShopifySharp.Tests/ShopifySmartCollectionService Tests/When_getting_a_collection.cs @@ -0,0 +1,41 @@ +using Machine.Specifications; + +namespace ShopifySharp.Tests.ShopifySmartCollectionService_Tests +{ + [Subject(typeof(ShopifySmartCollectionService))] + class When_getting_a_smart_collection + { + Establish context = () => + { + CollectionId = Service.CreateAsync(SmartCollectionUtils.CreateCollection()).Await().AsTask.Result.Id; + }; + + Because of = () => + { + Collection = Service.GetAsync(CollectionId.Value).Await(); + }; + + It should_get_a_smart_collection = () => + { + Collection.ShouldNotBeNull(); + Collection.Id.HasValue.ShouldBeTrue(); + Collection.BodyHtml.ShouldEqual(SmartCollectionUtils.BodyHtml); + Collection.Handle.ShouldBeEqualIgnoringCase(SmartCollectionUtils.Handle); + Collection.Title.ShouldEqual(SmartCollectionUtils.Title); + }; + + Cleanup after = () => + { + if (Collection != null && Collection.Id.HasValue) + { + Service.DeleteAsync(Collection.Id.Value).Await(); + } + }; + + static ShopifySmartCollectionService Service = new ShopifySmartCollectionService(Utils.MyShopifyUrl, Utils.AccessToken); + + static ShopifySmartCollection Collection; + + static long? CollectionId; + } +} diff --git a/ShopifySharp.Tests/ShopifySmartCollectionService Tests/When_listing_smart_collections.cs b/ShopifySharp.Tests/ShopifySmartCollectionService Tests/When_listing_smart_collections.cs new file mode 100644 index 000000000..453e6a706 --- /dev/null +++ b/ShopifySharp.Tests/ShopifySmartCollectionService Tests/When_listing_smart_collections.cs @@ -0,0 +1,50 @@ +using Machine.Specifications; +using System.Collections.Generic; +using System.Linq; + +namespace ShopifySharp.Tests.ShopifySmartCollectionService_Tests +{ + [Subject(typeof(ShopifySmartCollectionService))] + class When_listing_a_smart_collection + { + Establish context = () => + { + CreatedId = Service.CreateAsync(SmartCollectionUtils.CreateCollection()).Await().AsTask.Result.Id; + }; + + Because of = () => + { + Collections = Service.ListAsync().Await().AsTask.Result; + }; + + It should_create_a_smart_collection = () => + { + Collections.ShouldNotBeNull(); + Collections.Count().ShouldBeGreaterThanOrEqualTo(1); + Collections.Any(collection => + { + collection.Id.HasValue.ShouldBeTrue(); + collection.BodyHtml.ShouldEqual(SmartCollectionUtils.BodyHtml); + collection.Handle.ShouldBeEqualIgnoringCase(SmartCollectionUtils.Handle); + collection.Title.ShouldEqual(SmartCollectionUtils.Title); + + // .Should checks will throw an exception if false + return true; + }).ShouldBeTrue(); + }; + + Cleanup after = () => + { + if (CreatedId.HasValue) + { + Service.DeleteAsync(CreatedId.Value).Await(); + } + }; + + static ShopifySmartCollectionService Service = new ShopifySmartCollectionService(Utils.MyShopifyUrl, Utils.AccessToken); + + static IEnumerable Collections; + + static long? CreatedId; + } +} diff --git a/ShopifySharp.Tests/ShopifySmartCollectionService Tests/When_updating_a_smart_collection.cs b/ShopifySharp.Tests/ShopifySmartCollectionService Tests/When_updating_a_smart_collection.cs new file mode 100644 index 000000000..e71c97a92 --- /dev/null +++ b/ShopifySharp.Tests/ShopifySmartCollectionService Tests/When_updating_a_smart_collection.cs @@ -0,0 +1,42 @@ +using Machine.Specifications; + +namespace ShopifySharp.Tests.ShopifySmartCollectionService_Tests +{ + [Subject(typeof(ShopifySmartCollectionService))] + class When_updating_a_smart_collection + { + Establish context = () => + { + Collection = Service.CreateAsync(SmartCollectionUtils.CreateCollection()).Await(); + }; + + Because of = () => + { + Collection.BodyHtml = UpdatedHtml; + Collection = Service.UpdateAsync(Collection).Await(); + }; + + It should_update_a_smart_collection = () => + { + Collection.ShouldNotBeNull(); + Collection.Id.HasValue.ShouldBeTrue(); + Collection.BodyHtml.ShouldEqual(UpdatedHtml); + Collection.Handle.ShouldBeEqualIgnoringCase(SmartCollectionUtils.Handle); + Collection.Title.ShouldEqual(SmartCollectionUtils.Title); + }; + + Cleanup after = () => + { + if (Collection != null && Collection.Id.HasValue) + { + Service.DeleteAsync(Collection.Id.Value).Await(); + } + }; + + static string UpdatedHtml = "

Updated collection HTML

"; + + static ShopifySmartCollectionService Service = new ShopifySmartCollectionService(Utils.MyShopifyUrl, Utils.AccessToken); + + static ShopifySmartCollection Collection; + } +} diff --git a/ShopifySharp/Entities/ShopifySmartCollection.cs b/ShopifySharp/Entities/ShopifySmartCollection.cs index b920fdc83..ca38b6296 100644 --- a/ShopifySharp/Entities/ShopifySmartCollection.cs +++ b/ShopifySharp/Entities/ShopifySmartCollection.cs @@ -1,9 +1,6 @@ using Newtonsoft.Json; using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace ShopifySharp { @@ -17,8 +14,7 @@ public class ShopifySmartCollection : ShopifyObject ///
[JsonProperty("body_html")] public string BodyHtml { get; set; } - - + /// /// A human-friendly unique string for the smart collection automatically generated from its title. This is used in shop themes by the Liquid templating language to refer to the smart collection. Limit of 255 characters. /// @@ -29,7 +25,7 @@ public class ShopifySmartCollection : ShopifyObject /// The collection image. /// [JsonProperty("image")] - public string Image { get; set; } + public ShopifySmartCollectionImage Image { get; set; } /// /// This can have two different types of values, depending on whether the smart collection has been published (i.e., made visible to customers): @@ -40,7 +36,7 @@ public class ShopifySmartCollection : ShopifyObject public DateTime? PublishedAt { get; set; } /// - /// The sales channels in which the smart collection is visible. + /// The sales channels in which the smart collection is visible. The only currently known value is 'global'. /// [JsonProperty("published_scope")] public string PublishedScope { get; set; } @@ -58,7 +54,7 @@ public class ShopifySmartCollection : ShopifyObject public bool Disjunctive { get; set; } /// - /// The order in which products in the smart collection appear + /// The order in which products in the smart collection appear. Known values are 'alpha-asc', 'alpha-desc', 'best-selling', 'created', 'created-desc', 'manual', 'price-asc', 'price-desc'. /// [JsonProperty("sort_order")] public string SortOrder { get; set; } diff --git a/ShopifySharp/Entities/ShopifySmartCollectionImage.cs b/ShopifySharp/Entities/ShopifySmartCollectionImage.cs new file mode 100644 index 000000000..38ed24baa --- /dev/null +++ b/ShopifySharp/Entities/ShopifySmartCollectionImage.cs @@ -0,0 +1,29 @@ +using Newtonsoft.Json; +using System; + +namespace ShopifySharp +{ + /// + /// An object representing the image for a . + /// + public class ShopifySmartCollectionImage + { + /// + /// The date the image was created. + /// + [JsonProperty("created_at")] + public DateTime? CreatedAt { get; set; } + + /// + /// The image's source URL. + /// + [JsonProperty("src")] + public string Src { get; set; } + + /// + /// The image's base64 attachment, used when creating an image. + /// + [JsonProperty("attachment")] + public string Attachment { get; set; } + } +} diff --git a/ShopifySharp/Entities/ShopifySmartCollectionRules.cs b/ShopifySharp/Entities/ShopifySmartCollectionRules.cs index a82b687ca..f2a3e82b4 100644 --- a/ShopifySharp/Entities/ShopifySmartCollectionRules.cs +++ b/ShopifySharp/Entities/ShopifySmartCollectionRules.cs @@ -1,9 +1,4 @@ using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace ShopifySharp { @@ -12,7 +7,6 @@ namespace ShopifySharp /// public class ShopifySmartCollectionRules { - /// /// The relationship between the column choice, and the condition. /// diff --git a/ShopifySharp/Filters/ShopifySmartCollectionFilter.cs b/ShopifySharp/Filters/ShopifySmartCollectionFilter.cs new file mode 100644 index 000000000..34b3b6e5f --- /dev/null +++ b/ShopifySharp/Filters/ShopifySmartCollectionFilter.cs @@ -0,0 +1,28 @@ +using Newtonsoft.Json; + +namespace ShopifySharp.Filters +{ + /// + /// An object for filtering the results of and . + /// + public class ShopifySmartCollectionFilter : ShopifyPublishableListFilter + { + /// + /// Filter to smart collections with the given title. + /// + [JsonProperty("title")] + public string Title { get; set; } + + /// + /// Filter by smart collection handle. + /// + [JsonProperty("handle")] + public string Handle { get; set; } + + /// + /// Filter to smart collections that includes given product. + /// + [JsonProperty("product_id")] + public long? ProductId { get; set; } + } +} diff --git a/ShopifySharp/Services/SmartCollection/ShopifySmartCollectionService.cs b/ShopifySharp/Services/SmartCollection/ShopifySmartCollectionService.cs new file mode 100644 index 000000000..24982e12d --- /dev/null +++ b/ShopifySharp/Services/SmartCollection/ShopifySmartCollectionService.cs @@ -0,0 +1,100 @@ +using RestSharp; +using ShopifySharp.Filters; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace ShopifySharp +{ + /// + /// A service for manipulating Shopify's smart collections. + /// + public class ShopifySmartCollectionService : ShopifyService + { + /// + /// Creates a new instance of . + /// + /// The shop's *.myshopify.com URL. + /// An API access token for the shop. + public ShopifySmartCollectionService(string myShopifyUrl, string shopAccessToken) : base(myShopifyUrl, shopAccessToken) { } + + /// + /// Gets a count of all smart collections on the store. + /// + /// Options for filtering the count. + public async Task CountAsync(ShopifySmartCollectionFilter filterOptions = null) + { + var req = RequestEngine.CreateRequest("smart_collections/count.json", Method.GET, "count"); + + if (filterOptions != null) + { + req.Parameters.AddRange(filterOptions.ToParameters(ParameterType.GetOrPost)); + } + + return await RequestEngine.ExecuteRequestAsync(_RestClient, req); + } + + /// + /// Gets a list of up to 250 smart collections. + /// + /// Options for filtering the result. + public async Task> ListAsync(ShopifySmartCollectionFilter filterOptions = null) + { + var req = RequestEngine.CreateRequest($"smart_collections.json", Method.GET, "smart_collections"); + + if (filterOptions != null) + { + req.Parameters.AddRange(filterOptions.ToParameters(ParameterType.GetOrPost)); + } + + return await RequestEngine.ExecuteRequestAsync>(_RestClient, req); + } + + /// + /// Retrieves the with the given id. + /// + /// The id of the smart collection to retrieve. + public async Task GetAsync(long collectionId) + { + var req = RequestEngine.CreateRequest($"smart_collections/{collectionId}.json", Method.GET, "smart_collection"); + + return await RequestEngine.ExecuteRequestAsync(_RestClient, req); + } + + /// + /// Creates a new . + /// + /// A new . Id should be set to null. + public async Task CreateAsync(ShopifySmartCollection collection) + { + var req = RequestEngine.CreateRequest($"smart_collections.json", Method.POST, "smart_collection"); + + req.AddJsonBody(new { smart_collection = collection }); + + return await RequestEngine.ExecuteRequestAsync(_RestClient, req); + } + + /// + /// Updates the given . Id must not be null. + /// + /// The risk to update. + public async Task UpdateAsync(ShopifySmartCollection collection) + { + var req = RequestEngine.CreateRequest($"smart_collections/{collection.Id.Value}.json", Method.PUT, "smart_collection"); + + req.AddJsonBody(new { smart_collection = collection }); + + return await RequestEngine.ExecuteRequestAsync(_RestClient, req); + } + + /// + /// Deletes a smart collection with the given Id. + /// + /// The smart collection's id. + public async Task DeleteAsync(long collectionId) + { + var req = RequestEngine.CreateRequest($"smart_collections/{collectionId}.json", Method.DELETE); + + await RequestEngine.ExecuteRequestAsync(_RestClient, req); + } + } +} diff --git a/ShopifySharp/ShopifySharp.csproj b/ShopifySharp/ShopifySharp.csproj index 616da2204..07e0fc4ea 100644 --- a/ShopifySharp/ShopifySharp.csproj +++ b/ShopifySharp/ShopifySharp.csproj @@ -75,6 +75,7 @@ + @@ -91,6 +92,7 @@ + @@ -139,6 +141,7 @@ + diff --git a/readme.md b/readme.md index 5bfd8c104..5d0447018 100644 --- a/readme.md +++ b/readme.md @@ -74,6 +74,7 @@ With that said, this library is still pretty new. It currently suppports the fol - [Product Images](#product-images) - [Locations](#locations) - [Events](#events) +- [SmartCollections](#smart-collections) More functionality will be added each week until it reachs full parity with Shopify's REST API. @@ -1652,6 +1653,67 @@ var subjectType = "Order"; var orderEvents = await service.ListAsync(orderId, subjectType); ``` +## Smart Collections + +A smart collection is a grouping of products defined by simple rules set by shop owners. A shop owner creates a smart collection and then sets the rules that determine which products go in them. Shopify automatically changes the contents of smart collections based on their rules. + +### Creating a Smart Collection + +```cs +var service = new ShopifySmartCollectionService(myShopifyUrl, shopAccessToken); +var smartCollection = await service.CreateAsync(new ShopifySmartCollection() +{ + Title = "My Smart Collection", + Handle = "my-url-slug", + BodyHtml = "\Hello world!\", + Image = new ShopifySmartCollectionImage() + { + // Base-64 image attachment + Attachment = "R0lGODlhAQABAIAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==\n" + } +}); +``` + +### Updating a Smart Collection + +```cs +var service = new ShopifySmartCollectionService(myShopifyUrl, shopAccessToken); +var smartCollection = await service.GetAsync(smartCollectionId); + +smartCollection.Title = "My Updated Title"; + +smartCollection = await service.UpdateAsync(smartCollection); +``` + +### Getting a Smart Collection + +```cs +var service = new ShopifySmartCollectionService(myShopifyUrl, shopAccessToken); +var smartCollection = await service.GetAsync(smartCollectionId); +``` + +### Counting Smart Collections + +```cs +var service = new ShopifySmartCollectionService(myShopifyUrl, shopAccessToken); +var count = await service.CountAsync(); +``` + +### Listing Smart Collections + +```cs +var service = new ShopifySmartCollectionService(myShopifyUrl, shopAccessToken); +var smartCollections = await service.ListAsync(); +``` + +### Deleting a Smart Collection + +```cs +var service = new ShopifySmartCollectionService(myShopifyUrl, shopAccessToken); + +await service.DeleteAsync(smartCollectionId); +``` + # "Why don't you use enums?" I'm a big fan of using enums to make things easier for C# devs, because it removes a lot of the headache that comes with trying to remember all the valid string options for certain properties. With enums, we get those options hardcoded by default. We can easily scroll up and down the list of known values and select the one we need, without having to worry about typos. From 25061ecf9684b0a5f65bc7fae4954652e17c8dd9 Mon Sep 17 00:00:00 2001 From: Joshua Harms Date: Fri, 7 Oct 2016 10:28:18 -0500 Subject: [PATCH 08/10] Add ShopifyProductVariantService Closes #73 --- .../Playlists/Variants.playlist | 1 + .../Utils.cs | 55 +++++++++++ .../When_counting_product_variants.cs | 33 +++++++ .../When_creating_product_variants.cs | 38 +++++++ .../When_deleting_a_product_variant.cs | 36 +++++++ .../When_getting_a_product_variant.cs | 41 ++++++++ .../When_listing_product_variants.cs | 40 ++++++++ .../When_updating_product_variants.cs | 42 ++++++++ ShopifySharp.Tests/ShopifySharp.Tests.csproj | 7 ++ .../Filters/ShopifySmartCollectionFilter.cs | 2 +- .../ShopifyProductVariantService.cs | 98 +++++++++++++++++++ .../ShopifySmartCollectionService.cs | 4 +- ShopifySharp/ShopifySharp.csproj | 1 + readme.md | 58 ++++++++++- 14 files changed, 452 insertions(+), 4 deletions(-) create mode 100644 ShopifySharp.Tests/Playlists/Variants.playlist create mode 100644 ShopifySharp.Tests/ShopifyProductVariantService Tests/Utils.cs create mode 100644 ShopifySharp.Tests/ShopifyProductVariantService Tests/When_counting_product_variants.cs create mode 100644 ShopifySharp.Tests/ShopifyProductVariantService Tests/When_creating_product_variants.cs create mode 100644 ShopifySharp.Tests/ShopifyProductVariantService Tests/When_deleting_a_product_variant.cs create mode 100644 ShopifySharp.Tests/ShopifyProductVariantService Tests/When_getting_a_product_variant.cs create mode 100644 ShopifySharp.Tests/ShopifyProductVariantService Tests/When_listing_product_variants.cs create mode 100644 ShopifySharp.Tests/ShopifyProductVariantService Tests/When_updating_product_variants.cs create mode 100644 ShopifySharp/Services/ProductVariant/ShopifyProductVariantService.cs diff --git a/ShopifySharp.Tests/Playlists/Variants.playlist b/ShopifySharp.Tests/Playlists/Variants.playlist new file mode 100644 index 000000000..9200f26b7 --- /dev/null +++ b/ShopifySharp.Tests/Playlists/Variants.playlist @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ShopifySharp.Tests/ShopifyProductVariantService Tests/Utils.cs b/ShopifySharp.Tests/ShopifyProductVariantService Tests/Utils.cs new file mode 100644 index 000000000..f67b4d08a --- /dev/null +++ b/ShopifySharp.Tests/ShopifyProductVariantService Tests/Utils.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace ShopifySharp.Tests.ShopifyProductVariantService_Tests +{ + static class VariantUtils + { + static long? ProductId { get; set; } + + public static ShopifyProductVariantService Service = new ShopifyProductVariantService(Utils.MyShopifyUrl, Utils.AccessToken); + + public static string Option1 { get; } = "blue"; + + public static double Price { get; } = 123.45; + + public static ShopifyProductVariant CreateVariant() + { + return new ShopifyProductVariant() + { + Option1 = Option1, + Price = Price, + Metafields = new List() + { + new ShopifyMetaField() + { + Key = "new", + Value = "newvalue", + ValueType = "string", + Namespace = "global", + } + } + }; + } + + public static async Task GetProductId() + { + if (ProductId.HasValue) + { + return ProductId.Value; + } + + var service = new ShopifyProductService(Utils.MyShopifyUrl, Utils.AccessToken); + var products = await service.ListAsync(new Filters.ShopifyProductFilter() + { + Limit = 1, + Fields = "id" + }); + + ProductId = products.First().Id; + + return ProductId.Value; + } + } +} diff --git a/ShopifySharp.Tests/ShopifyProductVariantService Tests/When_counting_product_variants.cs b/ShopifySharp.Tests/ShopifyProductVariantService Tests/When_counting_product_variants.cs new file mode 100644 index 000000000..03d30092d --- /dev/null +++ b/ShopifySharp.Tests/ShopifyProductVariantService Tests/When_counting_product_variants.cs @@ -0,0 +1,33 @@ +using Machine.Specifications; +using System.Linq; + +namespace ShopifySharp.Tests.ShopifyProductVariantService_Tests +{ + [Subject(typeof(ShopifyProductVariantService))] + class When_counting_product_variants + { + Establish context = () => + { + ProductId = VariantUtils.GetProductId().Await(); + }; + + Because of = () => + { + Count = VariantUtils.Service.CountAsync(ProductId).Await(); + }; + + It should_count_product_variants = () => + { + Count.HasValue.ShouldBeTrue(); + }; + + Cleanup after = () => + { + + }; + + static int? Count; + + static long ProductId; + } +} diff --git a/ShopifySharp.Tests/ShopifyProductVariantService Tests/When_creating_product_variants.cs b/ShopifySharp.Tests/ShopifyProductVariantService Tests/When_creating_product_variants.cs new file mode 100644 index 000000000..979ca428c --- /dev/null +++ b/ShopifySharp.Tests/ShopifyProductVariantService Tests/When_creating_product_variants.cs @@ -0,0 +1,38 @@ +using Machine.Specifications; + +namespace ShopifySharp.Tests.ShopifyProductVariantService_Tests +{ + [Subject(typeof(ShopifyProductVariantService))] + class When_creating_product_variants + { + Establish context = () => + { + ProductId = VariantUtils.GetProductId().Await(); + }; + + Because of = () => + { + Variant = VariantUtils.Service.CreateAsync(ProductId, VariantUtils.CreateVariant()).Await(); + }; + + It should_create_a_product_variant = () => + { + Variant.ShouldNotBeNull(); + Variant.Id.HasValue.ShouldBeTrue(); + Variant.Option1.ShouldEqual(VariantUtils.Option1); + Variant.Price.ShouldEqual(VariantUtils.Price); + }; + + Cleanup after = () => + { + if (Variant != null && Variant.Id.HasValue) + { + VariantUtils.Service.DeleteAsync(ProductId, Variant.Id.Value).Await(); + } + }; + + static ShopifyProductVariant Variant; + + static long ProductId; + } +} diff --git a/ShopifySharp.Tests/ShopifyProductVariantService Tests/When_deleting_a_product_variant.cs b/ShopifySharp.Tests/ShopifyProductVariantService Tests/When_deleting_a_product_variant.cs new file mode 100644 index 000000000..76bef7a60 --- /dev/null +++ b/ShopifySharp.Tests/ShopifyProductVariantService Tests/When_deleting_a_product_variant.cs @@ -0,0 +1,36 @@ +using Machine.Specifications; +using System; + +namespace ShopifySharp.Tests.ShopifyProductVariantService_Tests +{ + [Subject(typeof(ShopifyProductVariantService))] + class When_deleting_a_product_variant + { + Establish context = () => + { + ProductId = VariantUtils.GetProductId().Await(); + VariantId = VariantUtils.Service.CreateAsync(ProductId, VariantUtils.CreateVariant()).Await().AsTask.Result.Id.Value; + }; + + Because of = () => + { + Ex = Catch.Exception(() => VariantUtils.Service.DeleteAsync(ProductId, VariantId)); + }; + + It should_delete_a_product_variant = () => + { + Ex.ShouldBeNull(); + }; + + Cleanup after = () => + { + + }; + + static Exception Ex; + + static long ProductId; + + static long VariantId; + } +} diff --git a/ShopifySharp.Tests/ShopifyProductVariantService Tests/When_getting_a_product_variant.cs b/ShopifySharp.Tests/ShopifyProductVariantService Tests/When_getting_a_product_variant.cs new file mode 100644 index 000000000..26c277655 --- /dev/null +++ b/ShopifySharp.Tests/ShopifyProductVariantService Tests/When_getting_a_product_variant.cs @@ -0,0 +1,41 @@ +using Machine.Specifications; + +namespace ShopifySharp.Tests.ShopifyProductVariantService_Tests +{ + [Subject(typeof(ShopifyProductVariantService))] + class When_getting_product_variants + { + Establish context = () => + { + ProductId = VariantUtils.GetProductId().Await(); + VariantId = VariantUtils.Service.CreateAsync(ProductId, VariantUtils.CreateVariant()).Await().AsTask.Result.Id.Value; + }; + + Because of = () => + { + Variant = VariantUtils.Service.GetAsync(VariantId).Await(); + }; + + It should_get_a_product_variant = () => + { + Variant.ShouldNotBeNull(); + Variant.Id.HasValue.ShouldBeTrue(); + Variant.Option1.ShouldEqual(VariantUtils.Option1); + Variant.Price.ShouldEqual(VariantUtils.Price); + }; + + Cleanup after = () => + { + if (Variant != null && Variant.Id.HasValue) + { + VariantUtils.Service.DeleteAsync(ProductId, Variant.Id.Value).Await(); + } + }; + + static ShopifyProductVariant Variant; + + static long ProductId; + + static long VariantId; + } +} diff --git a/ShopifySharp.Tests/ShopifyProductVariantService Tests/When_listing_product_variants.cs b/ShopifySharp.Tests/ShopifyProductVariantService Tests/When_listing_product_variants.cs new file mode 100644 index 000000000..48de9a12b --- /dev/null +++ b/ShopifySharp.Tests/ShopifyProductVariantService Tests/When_listing_product_variants.cs @@ -0,0 +1,40 @@ +using Machine.Specifications; +using System.Collections.Generic; +using System.Linq; + +namespace ShopifySharp.Tests.ShopifyProductVariantService_Tests +{ + [Subject(typeof(ShopifyProductVariantService))] + class When_listing_product_variants + { + Establish context = () => + { + ProductId = VariantUtils.GetProductId().Await(); + VariantId = VariantUtils.Service.CreateAsync(ProductId, VariantUtils.CreateVariant()).Await().AsTask.Result.Id.Value; + }; + + Because of = () => + { + Variants = VariantUtils.Service.ListAsync(ProductId).Await().AsTask.Result; + }; + + It should_list_product_variants = () => + { + Variants.ShouldNotBeNull(); + Variants.Count().ShouldBeGreaterThanOrEqualTo(1); + Variants.ShouldEachConformTo(v => v.Id.HasValue); + Variants.Any(v => v.Option1.Equals(VariantUtils.Option1) && v.Price.Equals(VariantUtils.Price)).ShouldBeTrue(); + }; + + Cleanup after = () => + { + VariantUtils.Service.DeleteAsync(ProductId, VariantId).Await(); + }; + + static IEnumerable Variants; + + static long ProductId; + + static long VariantId; + } +} diff --git a/ShopifySharp.Tests/ShopifyProductVariantService Tests/When_updating_product_variants.cs b/ShopifySharp.Tests/ShopifyProductVariantService Tests/When_updating_product_variants.cs new file mode 100644 index 000000000..a444198c6 --- /dev/null +++ b/ShopifySharp.Tests/ShopifyProductVariantService Tests/When_updating_product_variants.cs @@ -0,0 +1,42 @@ +using Machine.Specifications; + +namespace ShopifySharp.Tests.ShopifyProductVariantService_Tests +{ + [Subject(typeof(ShopifyProductVariantService))] + class When_updating_product_variants + { + Establish context = () => + { + ProductId = VariantUtils.GetProductId().Await(); + Variant = VariantUtils.Service.CreateAsync(ProductId, VariantUtils.CreateVariant()).Await(); + }; + + Because of = () => + { + Variant.Price = UpdatedPrice; + + Variant = VariantUtils.Service.UpdateAsync(Variant).Await(); + }; + + It should_update_a_product_variant = () => + { + Variant.ShouldNotBeNull(); + Variant.Id.HasValue.ShouldBeTrue(); + Variant.Price.ShouldEqual(UpdatedPrice); + }; + + Cleanup after = () => + { + if (Variant != null && Variant.Id.HasValue) + { + VariantUtils.Service.DeleteAsync(ProductId, Variant.Id.Value).Await(); + } + }; + + static ShopifyProductVariant Variant; + + static long ProductId; + + static double UpdatedPrice = 543.21; + } +} diff --git a/ShopifySharp.Tests/ShopifySharp.Tests.csproj b/ShopifySharp.Tests/ShopifySharp.Tests.csproj index 747e9eb38..e182648c1 100644 --- a/ShopifySharp.Tests/ShopifySharp.Tests.csproj +++ b/ShopifySharp.Tests/ShopifySharp.Tests.csproj @@ -176,6 +176,13 @@ + + + + + + + diff --git a/ShopifySharp/Filters/ShopifySmartCollectionFilter.cs b/ShopifySharp/Filters/ShopifySmartCollectionFilter.cs index 34b3b6e5f..dd8cf5832 100644 --- a/ShopifySharp/Filters/ShopifySmartCollectionFilter.cs +++ b/ShopifySharp/Filters/ShopifySmartCollectionFilter.cs @@ -3,7 +3,7 @@ namespace ShopifySharp.Filters { /// - /// An object for filtering the results of and . + /// An object for filtering the results of and . /// public class ShopifySmartCollectionFilter : ShopifyPublishableListFilter { diff --git a/ShopifySharp/Services/ProductVariant/ShopifyProductVariantService.cs b/ShopifySharp/Services/ProductVariant/ShopifyProductVariantService.cs new file mode 100644 index 000000000..3564c74b5 --- /dev/null +++ b/ShopifySharp/Services/ProductVariant/ShopifyProductVariantService.cs @@ -0,0 +1,98 @@ +using RestSharp; +using ShopifySharp.Filters; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace ShopifySharp +{ + /// + /// A service for manipulating a Shopify product's variants. + /// + public class ShopifyProductVariantService : ShopifyService + { + /// + /// Creates a new instance of . + /// + /// The shop's *.myshopify.com URL. + /// An API access token for the shop. + public ShopifyProductVariantService(string myShopifyUrl, string shopAccessToken) : base(myShopifyUrl, shopAccessToken) { } + + /// + /// Gets a count of all variants belonging to the given product. + /// + /// The product that the variants belong to. + public async Task CountAsync(long productId) + { + var req = RequestEngine.CreateRequest($"products/{productId}/variants/count.json", Method.GET, "count"); + + return await RequestEngine.ExecuteRequestAsync(_RestClient, req); + } + + /// + /// Gets a list of variants belonging to the given product. + /// + /// The product that the variants belong to. + /// Options for filtering the result. + public async Task> ListAsync(long productId, ShopifyListFilter filterOptions = null) + { + var req = RequestEngine.CreateRequest($"products/{productId}/variants.json", Method.GET, "variants"); + + if (filterOptions != null) + { + req.Parameters.AddRange(filterOptions.ToParameters(ParameterType.GetOrPost)); + } + + return await RequestEngine.ExecuteRequestAsync>(_RestClient, req); + } + + /// + /// Retrieves the with the given id. + /// + /// The id of the product variant to retrieve. + public async Task GetAsync(long variantId) + { + var req = RequestEngine.CreateRequest($"variants/{variantId}.json", Method.GET, "variant"); + + return await RequestEngine.ExecuteRequestAsync(_RestClient, req); + } + + /// + /// Creates a new . + /// + /// The product that the new variant will belong to. + /// A new . Id should be set to null. + public async Task CreateAsync(long productId, ShopifyProductVariant variant) + { + var req = RequestEngine.CreateRequest($"products/{productId}/variants.json", Method.POST, "variant"); + + req.AddJsonBody(new { variant }); + + return await RequestEngine.ExecuteRequestAsync(_RestClient, req); + } + + /// + /// Updates the given . Id must not be null. + /// + /// The variant to update. + public async Task UpdateAsync(ShopifyProductVariant variant) + { + var req = RequestEngine.CreateRequest($"variants/{variant.Id.Value}.json", Method.PUT, "variant"); + + req.AddJsonBody(new { variant }); + + return await RequestEngine.ExecuteRequestAsync(_RestClient, req); + } + + /// + /// Deletes a product variant with the given Id. + /// + /// The product that the variant belongs to. + /// The product variant's id. + public async Task DeleteAsync(long productId, long variantId) + { + var req = RequestEngine.CreateRequest($"products/{productId}/variants/{variantId}.json", Method.DELETE); + + await RequestEngine.ExecuteRequestAsync(_RestClient, req); + } + } +} diff --git a/ShopifySharp/Services/SmartCollection/ShopifySmartCollectionService.cs b/ShopifySharp/Services/SmartCollection/ShopifySmartCollectionService.cs index 24982e12d..e4ad3b71a 100644 --- a/ShopifySharp/Services/SmartCollection/ShopifySmartCollectionService.cs +++ b/ShopifySharp/Services/SmartCollection/ShopifySmartCollectionService.cs @@ -11,7 +11,7 @@ namespace ShopifySharp public class ShopifySmartCollectionService : ShopifyService { /// - /// Creates a new instance of . + /// Creates a new instance of . /// /// The shop's *.myshopify.com URL. /// An API access token for the shop. @@ -76,7 +76,7 @@ public async Task CreateAsync(ShopifySmartCollection col /// /// Updates the given . Id must not be null. /// - /// The risk to update. + /// The smart collection to update. public async Task UpdateAsync(ShopifySmartCollection collection) { var req = RequestEngine.CreateRequest($"smart_collections/{collection.Id.Value}.json", Method.PUT, "smart_collection"); diff --git a/ShopifySharp/ShopifySharp.csproj b/ShopifySharp/ShopifySharp.csproj index 07e0fc4ea..8916ecd21 100644 --- a/ShopifySharp/ShopifySharp.csproj +++ b/ShopifySharp/ShopifySharp.csproj @@ -121,6 +121,7 @@ + diff --git a/readme.md b/readme.md index 5d0447018..956a246d4 100644 --- a/readme.md +++ b/readme.md @@ -74,7 +74,8 @@ With that said, this library is still pretty new. It currently suppports the fol - [Product Images](#product-images) - [Locations](#locations) - [Events](#events) -- [SmartCollections](#smart-collections) +- [Smart Collections](#smart-collections) +- [Product Variants](#product-variants) More functionality will be added each week until it reachs full parity with Shopify's REST API. @@ -1714,6 +1715,61 @@ var service = new ShopifySmartCollectionService(myShopifyUrl, shopAccessToken); await service.DeleteAsync(smartCollectionId); ``` +## Product Variants + +A product variant is a different version of a product, such as differing sizes or differing colors. Without product variants, you would have to treat the small, medium and large versions of a t-shirt as three separate products; product variants let you treat the small, medium and large versions of a t-shirt as variations of the same product. + +### Creating a Product Variant + +```cs +var service = new ShopifyProductVariantService(myShopifyUrl, shopAccessToken); +var variant = await service.CreateAsync(productId, new ShopifyProductVariant() +{ + Option1 = "blue", + Price = 123.45, +}); +``` + +### Getting a Product Variant + +```cs +var service = new ShopifyProductVariantService(myShopifyUrl, shopAccessToken); +var variant = await service.GetAsync(variantId); +``` + +### Updating a Product Variant + +```cs +var service = new ShopifyProductVariantService(myShopifyUrl, shopAccessToken); +var variant = await service.GetAsync(variantId); + +variant.Price = 543.21; + +variant = await service.UpdateAsync(variant); +``` + +### Listing Product Variants + +```cs +var service = new ShopifyProductVariantService(myShopifyUrl, shopAccessToken); +var variants = await service.ListAsync(productId); +``` + +### Counting Product Variants + +```cs +var service = new ShopifyProductVariantService(myShopifyUrl, shopAccessToken); +var count = await service.CountAsync(productId); +``` + +### Deleting a Product Variant + +```cs +var service = new ShopifyProductVariantService(myShopifyUrl, shopAccessToken); + +await service.DeleteAsync(productId, variantId); +``` + # "Why don't you use enums?" I'm a big fan of using enums to make things easier for C# devs, because it removes a lot of the headache that comes with trying to remember all the valid string options for certain properties. With enums, we get those options hardcoded by default. We can easily scroll up and down the list of known values and select the one we need, without having to worry about typos. From 494d9ed881dcf3a32e1bc82ff8465d360b2d0432 Mon Sep 17 00:00:00 2001 From: Joshua Harms Date: Fri, 7 Oct 2016 10:41:15 -0500 Subject: [PATCH 09/10] Document Order Risks --- readme.md | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 956a246d4..d2b21f820 100644 --- a/readme.md +++ b/readme.md @@ -74,6 +74,7 @@ With that said, this library is still pretty new. It currently suppports the fol - [Product Images](#product-images) - [Locations](#locations) - [Events](#events) +- [Order Risks](#order-risks) - [Smart Collections](#smart-collections) - [Product Variants](#product-variants) @@ -98,7 +99,6 @@ The following APIs are not yet implemented by ShopifySharp, but I'm slowly worki | [FulfillmentService](https://help.shopify.com/api/reference/fulfillmentservice) | Not [ShopifyFulfillmentService](https://github.com/nozzlegear/ShopifySharp/blob/master/ShopifySharp/Services/Fulfillment/ShopifyFulfillmentService.cs). | | [GiftCard](https://help.shopify.com/api/reference/gift_card) | Requires Shopify Plus. | | [Multipass](https://help.shopify.com/api/reference/multipass) | Requires Shopify Plus. | -| [OrderRisks](https://help.shopify.com/api/reference/order_risks) | | | [Policy](https://help.shopify.com/api/reference/policy) | | | [Province](https://help.shopify.com/api/reference/province) | | | [Refund](https://help.shopify.com/api/reference/refund) | | @@ -1654,6 +1654,58 @@ var subjectType = "Order"; var orderEvents = await service.ListAsync(orderId, subjectType); ``` +## Order Risks + +The Order risk assessment is used to indicate to a merchant the fraud checks that have been done on an order. + +### Create an Order Risk + +```cs +var service = new ShopifyOrderRiskService(myShopifyUrl, shopAccessToken); +var risk = await service.CreateAsync(orderId, new ShopifyOrderRisk() +{ + Message = "This looks risk!", + Score = (decimal)0.85, + Recommendation = "cancel", + Source = "External", + CauseCancel = false, + Display = true, +}); +``` + +### Get an Order Risk + +```cs +var service = new ShopifyOrderRiskService(myShopifyUrl, shopAccessToken); +var risk = await service.GetAsync(orderId, riskId); +``` + +### Update an Order Risk + +```cs +var service = new ShopifyOrderRiskService(myShopifyUrl, shopAccessToken); +var risk = await service.GetAsync(orderId, riskId); + +risk.Message = "An updated risk message"; + +risk = await service.UpdateAsync(orderId, risk); +``` + +### List Order Risks + +```cs +var service = new ShopifyOrderRiskService(myShopifyUrl, shopAccessToken); +var risks = await service.ListAsync(orderId); +``` + +### Delete an Order Risk + +```cs +var service = new ShopifyOrderRiskService(myShopifyUrl, shopAccessToken); + +await service.DeleteAsync(orderId, riskId); +``` + ## Smart Collections A smart collection is a grouping of products defined by simple rules set by shop owners. A shop owner creates a smart collection and then sets the rules that determine which products go in them. Shopify automatically changes the contents of smart collections based on their rules. From 1fe63c02d7e05b58fddec945023b5314b1bae8df Mon Sep 17 00:00:00 2001 From: Joshua Harms Date: Fri, 7 Oct 2016 10:41:27 -0500 Subject: [PATCH 10/10] Bump to 3.1 --- ShopifySharp/Properties/AssemblyInfo.cs | 4 ++-- ShopifySharp/ShopifySharp.nuspec | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/ShopifySharp/Properties/AssemblyInfo.cs b/ShopifySharp/Properties/AssemblyInfo.cs index 1439f7b09..8bc062e52 100644 --- a/ShopifySharp/Properties/AssemblyInfo.cs +++ b/ShopifySharp/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("3.0.0.0")] -[assembly: AssemblyFileVersion("3.0.0.0")] +[assembly: AssemblyVersion("3.1.0.0")] +[assembly: AssemblyFileVersion("3.1.0.0")] diff --git a/ShopifySharp/ShopifySharp.nuspec b/ShopifySharp/ShopifySharp.nuspec index 33af6c99e..44916d830 100644 --- a/ShopifySharp/ShopifySharp.nuspec +++ b/ShopifySharp/ShopifySharp.nuspec @@ -11,6 +11,14 @@ false ShopifySharp is a .NET library that enables you to authenticate and make API calls to Shopify. + 3.1.0 + ===== + - New ShopifyProductVariantService: Create, get, update, count, list and delete a product's variants [(#73)](https://github.com/nozzlegear/ShopifySharp/issues/73). + - New ShopifySmartCollectionService: Create, get, update, count, list and delete smart collections [(#72)](https://github.com/nozzlegear/ShopifySharp/issues/72). + - New ShopifyOrderRiskService: Create, get, update, list and delete order risks [(#71)](https://github.com/nozzlegear/ShopifySharp/issues/71). + - When an API call reaches Shopify's rate limit, a ShopifyRateLimitException will now be thrown. This exception inherits from the base ShopifyException, so previous code will still catch the exception [(#67)](https://github.com/nozzlegear/ShopifySharp/issues/67). + - Bugfix: ShopifySharp will not properly parse the "authorization code was not found or was already used" error when trying to reuse an OAuth code [(#70)](https://github.com/nozzlegear/ShopifySharp/issues/70). + - Bugfix: Closing and opening an order should return the ShopifyOrder object [(#76)](https://github.com/nozzlegear/ShopifySharp/issues/76). 3.0.0 ===== - Breaking release: all enums have been removed and replaced with strings to avoid unannounced changes from Shopify breaking your apps. Reasoning for this change is documented at https://github.com/nozzlegear/ShopifySharp/pull/65.