GithubHelp home page GithubHelp logo

ordercloud-dotnet-sdk's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

ordercloud-dotnet-sdk's Issues

Specify model type, not xp type, in endpoint methods that return models with strongly typed xp

Some models with xp have nested model properties that also have xp (i.e. Order.ShippingAddress.xp). Currently, if you want to fetch these from the API with strongly typed xp, you need to specify all xp types on every call. This gets tedious and repetitive:

var order = client.Orders.GetAsync<OrderXP, UserXP, AddressXP>(...)
var lineItems = client.LineItems.ListAsync<LineItemXP, ProductXP, AddressXP, AddressXP>(...);
client.LineItems.PatchAsync<LineItemXP, ProductXP, AddressXP, AddressXP>(...);

Proposed change: Specify the full model type instead of the xp types. This allows you to simplify the calls above, but it first requires defining your own types that inherit from strongly-typed models:

public class MyOrder : Order<OrderXP, UserXP, AddressXP>
{ }

public class MyLineItem : LineItem<LineItemXP, ProductXP, AddressXP, AddressXP>
{ }

With those in place, you could now do this:

var order = client.Orders.GetAsync<MyOrder >(...)
var lineItems = client.LineItems.ListAsync<MyLineItem>(...);
client.LineItems.PatchAsync<MyLineItem>(...);

Although defining inherited types is encouraged, it could feel like overkill in some scenarios, so if you want you can still do this:

var address = client.Addresses.Get<Address<AddressXP>>(...);

BUG: Type error when trying to use partial models with custom XP

Consider the following models:

    public class WinmarkLineItem: LineItem<LineItemXP, ProductXP, ShippingAddressXP, ShipFromAddressXP>
    {}

    public class PartialWinmarkLineItem: PartialLineItem<LineItemXP, ProductXP, ShippingAddressXP, ShipFromAddressXP>
    {} 

And the attempted call:

public async Task<WinmarkLineItem> Patch(string orderID, string lineItemID, PartialWinmarkLineItem partialLI, string clientID)
{
    return await _oc.LineItems.PatchAsync<PartialWinmarkLineItem>(OrderDirection.Incoming, orderID, lineItemID, partialLI);
}

I get an error that I cannot convert from PartialWinmarkLineItem to OrderCloud's LineItem. Possible cause for this error (per Todd):

In a nutshell, the problem is that PartialLineItem<T1, T2, T3, T4> inherits from LineItem, when (I believe) it should inherit from PartialLineItem.

String-based search, sort, and filter fields in query DSL

The query DSL allows you to use strongly-typed property expressions to specify search, sort, and filter fields. But in some cases, like with nested fields and xp, this can be awkward or impossible. This feature allows you to get a little closer to the metal without giving up the whole DSL by providing overloads to specify properties and values as the exact strings that will get added to the URI query:

await Client.Me.ListProductsAsync(opts => opts
    .SearchFor("foo")
    .SearchOn("Name", "Description", "xp.SomeText")
    .SortBy("xp.ListOrder")
    .AddFilter("xp.Version", ">2"));

WebhookPayloads don't have strongly typed xp

You can see here xp is of type dynamic
not-strongly-typed-xp

Type arguments for webhookpayload allow for strongly typed config data but nothing for xp
configdata

I would expect to be able to get a strongly typed order by doing something like this:

WebhookPayloads.Order<MyCustomXP>.Cancel<MyCustomConfig>

Add a public constructor to OrderCloudException

My project using this SDK has catch blocks where I do certain logic if certain OrderCloudExceptions are thrown. In my unit tests I would like to be able to create an OrderCloudException object so that I can mock a failure response from OrderCloud. However, OrderCloudException has no public constructor.

Could we add public constructor to OrderCloudException? Thank you!

How to implement Faceted Product Search

I have list of string field types in Product xp field and I was trying to add Filter on these list type fields with OR condition as below

var searchText = "UserEnteredText";
var searchFields ="Name";
var filters = "xp.FieldOne = FieldOneValue1|FieldOneValue2 || xp.FieldTwo = FieldTwoValue1|FieldTwoValue2";

var products = await _oc.Me.ListProductsAsync(filters: filters, page: args.Page, search: searchText, searchOn: searchFields, searchType: SearchType.AnyTerm, sortBy: sortBy);

But it is not returning the expected result.

Is there any other approach which can achieve the above scenario.Any guidance will be much appreciated.

Webhook Payloads - Add generic arguments for Request and Response body (BREAKING)

Strongly typed webhook payloads Make it easier to build webhook listeners in middleware, including the (optional) ability to use a custom type for ConfigData.

But let's say you have a class that defines a custom type for xp:

public class MyAddress : Address<CustomXp> { }

You could define a handler in your middleware that listens on address create:

public Task HandleAddressCreate(WebhookPayloads.Addresses.Create<MyConfigData> payload)

And you can access the created Address via payload.Request.Body. But there's no (easy) way to coerce that into a MyAddress and get the strongly-typed xp.

To address this, all generic payload definitions (which currently just have a TConfigData generic arg) will now provide generic args for the Request and/or Response body when the endpoint defines them. So the above example becomes:

public Task HandleAddressCreate(
    WebhookPayloads.Addresses.Create<MyConfigData, MyAddress> payload)

And payload.Request.Body now contains your strongly typed xp.

Consistent with #52, you'll need to provide the Address base type in the case where you want a strongly typed ConfigData but not a strongly typed Address.xp (and Intellisense will inform you of which base types to use when you're not providing custom types):

public Task HandleAddressCreate(
    WebhookPayloads.Addresses.Create<MyConfigData, Address> payload)

Rename methods corresponding to PUT endpoints from "Update*" to "Save*"

This is a breaking change. We're generally moving away from using the word "Update" to describe PUT endpoints because:

  1. It's misleading. PUT can also be used for creating things where you provide the ID.
  2. It could discourage using PATCH, which is often the best choice for updating things.

WebhookPayloads with empty bodies fail to deserialize

When a Webhook payload does not expect a request body, such as those corresponding to most DELETE endpoints, the SDK's WebhookPayload.Request.Body property is typed as a string. But the OrderCloud API sends an empty object {} in the actual payload, which fails to deserialize to a string, causing deserialization of the entire payload to fail. Response bodies have the same convention.

Best fix is to change the expected type to object when an empty body is expected. This is the least error-prone as it will deserialize successfully whether the body is {}, null, or an empty string.

Make API-read-only properties writable

Although a nice "feature", making model properties that can't be written to the API (i.e. Order.Total) read-only at the C# level has proven to make those models harder to use in tests. For example, you might want test the behavior of your code when the total of a fake order exceeds some amount. The most straightforward solution is to just make them writable.

Note that there's no harm in sending a read-only property like Order.Total in a POST/PATCH/PUT. OrderCloud.io won't return an error, it'll just ignore it.

Config Options around Ordercloud Environments.

It would be nice to have config options around the three different environments. So that a new instance of OrdercloudClient could be created something like this

new OrderCloudClient(new OrderCloudClientConfig()
			{
				GrantType = GrantType.ClientCredentials,
                                Enviornment = OrderCloudEnvironment.Staging,
				ClientId = clientID,
				Roles =  new ApiRole[] { }
			}); 

List filters - 2 filters on the same property only sends the last one

Say you want to get a list of products with QuantityMutiplier greater than 5 and less than 20. This should work:

list = await client.Products.ListAsync(opts => opts
    .AddFilter(p => p.QuantityMultiplier > 5)
    .AddFilter(p => p.QuantityMultiplier < 20));

But currently only the last filter set gets sent.

Unhandled Error when the OC API returns a non-json response body.

In error situations when OC returns a raw string response body instead of a json response, the sdk method ThrowAuthExceptionAsync throws a not very helpful error "Object reference not set to an instance ....".

Here's code to re-create this error.

>             var oc = new OrderCloudClient(new OrderCloudClientConfig()
>             {
>                 ApiUrl = "https://sandboxapi.ordercloud.io/v1",
>                 AuthUrl = "https://sandboxapi.ordercloud.io/v1",
>                 ClientId = _settings.OrderCloudSettings.ClientID,
>                 ClientSecret = _settings.OrderCloudSettings.ClientSecret,
>                 Roles = new[] { ApiRole.FullAccess }
>             });
> 
>             var token = await oc.AuthenticateAsync();

The "/v1" in the url is incorrect and results in a response "The resource you are looking for has been removed, had its name changed, or is temporarily unavailable." from OC.

Potential bug

private void ValidateConfig() {
if (Config.GrantType == GrantType.Password) {
RequireConfigProp(c => c.ClientId);
RequireConfigProp(c => c.Username);
RequireConfigProp(c => c.Password);
}
else if (Config.GrantType == GrantType.Password) {
RequireConfigProp(c => c.ClientId);
RequireConfigProp(c => c.ClientSecret);
}
}

It both checks for GrantType.Password

Reset Password Endpoints Require Authentication

The following code:

static void Main(string[] args)
        {
            var client = new OrderCloudClient();
            Task.WaitAll(client.PasswordResets.SendVerificationCodeAsync(new PasswordResetRequest
            {
                ClientID = "client-app-id",
                Email = "my-email",
                URL = "http://reset-password-url.com"
            }));
        }

throws an exception:

Unhandled Exception: System.AggregateException: One or more errors occurred. ---> System.ArgumentException: clientID is required.
  at OrderCloud.SDK.OrderCloudClient.Require(String value, String name)
  at OrderCloud.SDK.OrderCloudClient.AuthenticateAsync(String clientID, String username, String password, ApiRole[] roles)
  at OrderCloud.SDK.OrderCloudClient.<AuthenticateAsync>d__150.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
  at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
  at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
  at OrderCloud.SDK.OrderCloudClient.<EnsureTokenAsync>d__160.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
  at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
  at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
  at Flurl.Http.FlurlRequest.<SendAsync>d__19.MoveNext()
  --- End of inner exception stack trace ---
  at System.Threading.Tasks.Task.WaitAll(Task[] tasks, Int32 millisecondsTimeout, CancellationToken cancellationToken)
  at System.Threading.Tasks.Task.WaitAll(Task[] tasks, Int32 millisecondsTimeout)
  at System.Threading.Tasks.Task.WaitAll(Task[] tasks)
  at TestOrderCloud.Program.Main(String[] args) in c:\projects\TestOrderCloud\TestOrderCloud\Program.cs:line 16 

This happens because reset password requests go through standard OC request infrastructure so it tries to authenticate before sending the request.
The expected behavior is that authentication is not required to reset password.

400 when trying to login as seller

If you will make a request with sellers credentials via SDK library, you get 400 error. Code to reproduce:

        const string clientId = "your_client_id";
        const string userName = "user_name";
        const string password = "user_password";

        var ocClient = new OrderCloudClient(new OrderCloudClientConfig
        {
            ClientId = clientId,
            Roles = new []{ ApiRole.FullAccess },
            Username = userName,
            Password = password,
            GrantType = GrantType.Password
        });

        // Call any method via OrderCloud.SDK library cause 400 error on auth.
        var userInfo = await ocClient.Me.GetAsync();

Call for User Group Assignments for a non-existent Buyer returns 200

If I make a call for a ListPage of UserGroupAssignment objects from a non-existent Buyer like this:

<>.UserGroups.ListUserAssignmentsAsync(<>);

If the buyerID argument is set to a Buyer that is non-existent in OrderCloud, instead of sending back a 404 response, it seems to send back a 200 response along with a ListPage of zero UserGroupAssignment objects.

Feature: Model validation including xp fields

Any kind of model validation on the OrderCloud model such as Required, MaxLength, etc. should be applied to models in the SDK as well, and similarly there should be a way to define if xp is required if any xp properties are also required or have some other validation associated with them.

Support strongly typed xp on nested models

Some models with strongly typed xp also have nested models with xp. For example: LineItem.ShippingAddress.xp. It would be nice to support strongly typed xp on those nested models. This will be supported via additional type parameters on relevant models and endpoint methods.

For example, creating a LineItem with strongly typed xp will now look like this:

new LineItem<Txp, TProductXP, TShippingAddressXP, TShipFromAddressXP>();

Getting a line item with strongly typed xp will look like this:

await client.LineItems.GetAsync<Txp, TProductXP, TShippingAddressXP, TShipFromAddressXP>(...);

Note that this is a breaking change. For example, this will no longer compile:

new LineItem<Txp>();

You must specify all type parameters or none. If some are not needed, or you want weakly typed xp for some, the convention will be to specify dynamic for the corresponding type parameters.

The way xp is modeled violates liskov substitution principle

Consider the following scenario:

public class MyOrder: Order<MyXp>{}
public class MyXp
{
    public string Color { get; set; }
}

var order = new MyOrder();
order = (Order)order;
order.xp = 5;
order = (MyOrder)order;
var color = order.xp.Color; // 'myOrder.xp.Color' threw an exception of type 'System.InvalidCastException'

The expectation would be that when I use the instance as MyOrder everything will be fine, but it cannot be fine since it is backed by the same dictionary instance and the implementation of ordercloud models does not handle this scenario.

This may seem a bit contrived but I came across this when trying to use AutoFixture to automatically mock ordercloud models with typed xp. AutoFixture cannot ignore the hidden properties, since the hidden property values can always be used, while its lower instance is cast to it's base type.

Filter serialization issue in v0.10.1

The filters string in v0.10.1 is serializing incorrectly, and causing issues in requests.

The following SDK method was used in two tests, one using SDK v0.10.1 and the other in v0.10.0.
await _oc.Me.ListProductsAsync<BuyerProduct>(filters: "xp.Facets.apparel=Tees%20%26%20Tanks", sortBy: "Name", accessToken: user.AccessToken);

In v0.10.0, the above method yields the following request URI (when observed via Fiddler). This URI is correct, and yields expected results.
GET /v1/me/products?searchType=AnyTerm&sortBy=Name&page=1&pageSize=20&xp.Facets.apparel=Tees%20%26%20Tanks HTTP/1.1

In v0.10.1, the above method yields the following URI, which is incorrect and causes unexpected results.
GET /v1/me/products?searchType=AnyTerm&sortBy=Name&page=1&pageSize=20&xp.Facets.apparel=Tees%2520%2526%2520Tanks HTTP/1.1

Allow custom ISerializer implementations to be used with SDK

Currently, the SDK has its own internal instance of NewtonsoftJsonSerializer with hardcoded serializer options configured. This makes it impossible to manipulate the behaviour of (de-)serialized xp objects.

There are a few possible options to address this:

  • use ISerializer instance from DI container (but this possibly interferes with use of OrderCloudContractResolver)
  • use internal serializer for OrderCloud objects, but allow custom ISerializer implementation to be used with xp objects, defaulting to the internal if one is not configured

Change to specifying generic xp types for nested properties (BREAKING)

This is a breaking change to how you specify strongly-typed xp for models that contain nested models. It's best illustrated with an example; we'll use Order, which has an xp property and also contains 2 nested models - FromUser and BillingAddress - that also contain xp.

Before Change

Currently, if you want to specify a strongly typed xp on Order, you also need to specify them for the nested properties:

Order<Txp, TFromUserXP, TBillingAddressXP>

A common (and encouraged) practice is to create a new custom type:

public class MyOrder : Order<Txp, TFromUserXP, TBillingAddressXP> { }

If you don't care about strong-typeing FromUser.xp and BillingAddress.xp, you would do this:

public class MyOrder : Order<Txp, dynamic, dynamic> { }

After Change

Instead of specifying custom types for xp of the nested properties, you will now specify custom types for the properties themselves. For example:

public class MyOrder : Order<Txp, MyUser, MyAddress> { }

This would require you to define these (which, as mentioned, is encouraged):

public class MyUser : User<Txp> { }
public class MyAddress : Address<Txp> { }

Although less encouraged, you certainly have the option of doing this if you don't want to define the 2 additional classes:

public class MyOrder : Order<Txp, User<TUserXp>, Address<TAddressXp>> { }

If you don't care about strong-typeing FromUser.xp and BillingAddress.xp, here's the new way:

public class MyOrder : Order<Txp, User, Address> { }

Intellisense will guide you through which specific base types to use in this cases.

Affected Classes

Non-generic model types and those without nested model properties are not affected. These classes are affected:

  • BuyerProduct<Txp, TPriceSchedule>
  • LineItem<Txp, TProduct, TVariant, TShippingAddress, TShipFromAddress>
  • Order<Txp, TFromUser, TBillingAddress>
  • Payment<Txp, TTransactions>
  • ShipEstimate<Txp, TShipMethods>
  • ShipEstimateResponse<Txp, TShipEstimates>
  • Shipment<Txp, TFromAddress, TToAddress>
  • ShipmentItem<Txp, TProduct, TVariant>
  • Spec<Txp, TOptions>
  • PartialLineItem<Txp, TProduct, TVariant, TShippingAddress, TShipFromAddress>
  • PartialOrder<Txp, TFromUser, TBillingAddress>
  • PartialPayment<Txp, TTransactions>
  • PartialShipment<Txp, TFromAddress, TToAddress>
  • PartialSpec<Txp, TOptions>

Although it's a good idea to check your code for these, generic constraints should catch anything that needs to be changed. For example, if you forget to change TBillingAddressXp to TBillingAddress, the compiler will complain that TBillingAddressXp is not a sub-type of Address.

Strongly typed order with custom models showing custom BillingAddress as null

I have the following models defined for my custom order:

public class OrderXP
{
    public bool? ConvertCrypto { get; set; } 
    public string ExpeditedShipping { get; set; }
    public string CustomerNumber { get; set; }
    public bool? SaveAddress { get; set; }
    public string[] PromoCodes { get; set; }
}

public class UserXP
{
    public string[] Locations { get; set; }
    public string[] FavoriteProducts { get; set; }
    public string[] FavoriteOrders { get; set; }
    public string orderingaccount { get; set; }
    public string Brand { get; set; }
    public AvailablePaymentMethod[] AvailablePaymentMethods { get; set; }
}

public class AddressXP
{
    public string Email { get; set; }
}

And am retrieving an order like so:

var order = await _oc.Orders.GetAsync<DOrder>(OrderDirection.Incoming, body.OrderID, accessToken: backofficeToken);

but order.BillingAddress is null. When I set a breakpoint and inspect I can see the following:
screenshot_14

It seems like my custom BillingAddress is coming through correctly but is overshadowed by the other BillingAddress that is set to null

This is the order I am getting for completeness:

{
  "DateCreated": "2019-02-20T14:27:56.757+00:00",
  "ID": "W1312915",
  "FromUser": {
    "ID": "chipotlecrhistian",
    "Username": "chipotlecrhistian",
    "Password": null,
    "FirstName": "crhistian",
    "LastName": "ramirez",
    "Email": "[email protected]",
    "Phone": "6128042692",
    "TermsAccepted": "2018-03-01T21:18:08.97+00:00",
    "Active": true,
    "xp": {
      "FavoriteProducts": [
        "1060",
        "1041",
        "testdummyproduct",
        "13454"
      ],
      "AvailablePaymentMethods": [
        {
          "Display": "Burgers",
          "Value": "PurchaseOrder"
        },
        {
          "Display": "Credit Card",
          "Value": "CreditCard"
        }
      ],
      "editableAddresses": [
        "T97463-0",
        "T56324-0",
        "T56306-0"
      ],
      "passwordchanged": true,
      "FavoriteOrders": [
        "W8424575"
      ],
      "Brand": null
    },
    "AvailableRoles": null
  },
  "FromCompanyID": "chipotle",
  "FromUserID": "chipotlecrhistian",
  "BillingAddressID": "T56324-0_billing",
  "BillingAddress": {
    "ID": "T56324-0_billing",
    "DateCreated": "2018-03-22T18:10:34.063+00:00",
    "CompanyName": "T56324-0_billing",
    "FirstName": null,
    "LastName": null,
    "Street1": "1644 E Evans",
    "Street2": "",
    "City": "Denver",
    "State": "CO",
    "Zip": "80210",
    "Country": "US",
    "Phone": "3037224121",
    "AddressName": "T56324-0_billing",
    "xp": {}
  },
  "ShippingAddressID": "T56324-0",
  "Comments": null,
  "LineItemCount": 1,
  "Status": "Unsubmitted",
  "DateSubmitted": null,
  "DateApproved": null,
  "DateDeclined": null,
  "DateCanceled": null,
  "DateCompleted": null,
  "Subtotal": 12.41,
  "ShippingCost": 0,
  "TaxCost": 0,
  "PromotionDiscount": 0,
  "Total": 12.41,
  "IsSubmitted": false,
  "xp": {
    "ExpeditedShipping": "56",
    "sellerOrderID": 0,
    "Over48": "no",
    "CustomerNumber": "T56324-0",
    "SaveAddress": false,
    "PaymentType": "CreditCard",
    "ApprovalRegion": "",
    "PromoCodes": []
  }
}

BUG- translating "!= null" in list options builder

Given a list call like this:

client.Product.ListAsync(opts => opts.AddFilter(p => p.ShipFromAddressID != null);

The resulting API call looks like this:

/products?ShipFromAddressID=!

"Not null" should translate to *, so we actually want this:

/products?ShipFromAddressID=*

Webhook callback models missing

There are a couple platform-defined models missing from the SDK. They are webhook or integration event callback models.

  • WebhookResponse
  • OrderCalculatePayload
  • OpenIDConnectUserPayload

Alternate pattern for specifying list endpoint arguments via builder pattern

This pattern also makes heavy use of expressions, allowing you to specify things like searchOn fields and filters in a strongly typed manner.

For example,

await Client.Me.ListProductsAsync(opts => opts
    .SearchFor("foo")
    .SearchOn(p => p.Name, p => p.Description)
    .SortBy(p => p.Name)
    .AddFilter(p => p.Active && p.ShipWeight < 100)
    .Page(1)
    .PageSize(100));

will send the following GET request:

v1/me/products?search=foo&searchOn=Name,Description&sortBy=Name&Active=true&ShipWeight=<100&page=1&pageSize=100

Filter expressions are powerful but there are a few caveats related to be aware of:

  • Only ==, <, >, <=, >=, != are supported.
  • && is supported and will add multiple filters as demonstrated above.
  • || is supported, but only on multiple checks against the same property, i.e. p.ID == "a" || p.ID == "b" produces ID=a|2 in the query string; p.ID == "a" || p.Name == "a" is not supported. (Consider using SearchFor/SearchOn for this scenario.)
  • The left side of a filter must be a simple property accessor, such as p.Active or p.ShipWeight above.
  • The right side of a filter cannot reference the filter subject. For example, p.ShipWeight == p.ShipHeight is not allowed; p cannot appear on the right side of the expression. (The right side can contain variables, math operators, function calls, etc, as long as it can be evaluated to single value at runtime.)

Additional caveat: Due to a C# limitation of not being able to mix dynamics and expressions, you can only use expression-based xp filters if you're also using strongly-typed xp, i.e. Client.Me.ListProductsAsync<MyXpType>(...).

Expose validation method

OrderCloudReource has an internal ValidateModel method that uses reflection to find all the properties marked [Required] and throw if those properties don't have a value. It would be handy sometimes to tap into this validation prior to calling an endpoint and without throwing an exception.

Enhanced exception messages

OrderCloudException.Message currently just echos the inner exception's Message, which is typically a generic HTTP-level error message and not very helpful. Instead, return the Message of the first object in the OrderCloudException.Errors collection (normally there's only one anyway).

When handling OrderCloudException explicitly it's still best to to inspect the Errors property, but this change will provide better error messages to things like 3rd-party logging frameworks that handle System.Exception more generally.

Password grant flow results in "invalid_client: invalid client secret"

var userClient = new OrderCloudClient(new OrderCloudClientConfig {
    ClientId = _userClientId,
    Username = _username,
    Password = _password,
    ApiUrl = "https://westeurope-sandbox.ordercloud.io",
    AuthUrl = "https://westeurope-sandbox.ordercloud.io",
    Roles = new[] { ApiRole.Shopper }
});

var resp = await userClient.AuthenticateAsync();

This code results in OrderCloud.SDK.OrderCloudException: invalid_client: invalid client secret. What does the client secret have to do with anything? Is the error message supposed to say invalid user password? (I'm positive the password is correct, so that shouldn't be the case either, but...)

By the way, is the above supposed to be a valid way of getting a user access token, or should I stick to using using a FullAccess client and ImpersonationConfigs instead?

Sync strongly-typed ("new") properties with base class

Say you define a custom type with a strongly-typed xp per this common pattern:

public class MyAddress : Address<MyXp> { }

MyAddress.xp doesn't technically override Address.xp - it hides it (via the new modifier), because it's a different type. Consequently, if you refer to a MyAddress as its base class Address, such as in a method call, the xp values set on the derived class won't be visible.

Even though xp on the base class and derived class are different, it's still possible via the property implementations to point them to the same value.

Here's a slightly more complex example. This test currently fails, but will pass with this fix:

class CustomAddress : Address<CustomXP> { }
class CustomLineItem : LineItem<CustomXP, ..., CustomAddress> { }

var customLI = new CustomLineItem {
    ShippingAddress = new CustomAddress {
        xp = new CustomXP { Foo = "hello" }
    }
};

AssertLineItem(customLI);

void AssertLineItem(LineItem li) {
    Assert.IsNotNull(li.ShippingAddress);
    Assert.IsNotNull(li.ShippingAddress.xp);
    Assert.AreEqual("hello", li.ShippingAddress.xp.Foo);
}

Updates to support new product facet endpoints

Includes:

  • ProductFacets resource and methods corresponding to CRUD endpoints.
  • ProductFacet model.
  • Roles: ProductFacetReader, ProductFacetAdmin.
  • Strongly typed WebhookPayloads corresponding to new endpoints.
  • Meta.Facets collection returned on client.Me.ListProductsAsync result.

Note that Product Facets, along with enhanced product search capabilities (powered by Elasticsearch) are currently in beta and must be "enabled" for your organization. Please contact Four51 for information on participating in the beta program.

Add webhook payload/response objects

When OrderCloud.io fires webhooks, it sends a JSON payload of a specific shape. The SDK should make available classes matching these shapes, making it easier to build custom endpoints that subscribe and respond to these webhooks.

Included will be these general-purpose types:

public class WebhookPayload
{
    public string Route { get; set; }
    public dynamic RouteParams { get; set; }
    public string Verb { get; set; }
    public DateTimeOffset? Date { get; set; }
    public string LogID { get; set; }
    public string UserToken { get; set; }
    public HttpMessage<dynamic> Request { get; set; }
    public HttpMessage<dynamic> Response { get; set; }
    public dynamic ConfigData { get; set; }
}

public class HttpMessage<TBody>
{
    public TBody Body { get; set; }
    public string Headers { get; set; }
}

Also included will be WebhookPayload<TConfigData>, which allows you to work with strongly-typed config data.

Even more useful will be a full set of classes for every endpoint that supports webhooks, so that you get strong typing of Request.Body, Response.Body, and RouteParams. These will be discoverable nested classes that follow a similar paradigm to Client.Resource.Endpoint used for the async HTTP methods in the API.

Here's an example of what an order submit webhook listener might look like in an ASP.NET Core project:

[Route("webhooks/orders/onsubmit")]
public Task HandleOrderSubmit([FromBody] WebhookPayloads.Orders.Submit<MyConfigData> payload)
{
    ...
}

In this example, payload.Request.Body is a strongly typed Order object. Similarly, RouteParams has strongly typed Direction and OrderID properties.

Finally, a type representing a response to a webhook configured as a pre-hook will be provided:

public class WebhookResponse
{
    public bool proceed { get; set; }
}

This will have static helper methods allowing a webhook listener to respond with:

return WebhookResponse.Proceed();

or:

return WebhookResponse.Block();

Refresh token is not used for refreshing access token

It is possible to store Token response in OrderCloud client so that it takes access token. Once access token expires, SDK tries to re-login using username/password or clientid/secret. However, the first thing it should do is using refresh token to try to get a new access token as that's what refresh token is for.

Allow authenticating (getting token) without changing state of OcClient

OcClient allows you to optionally provide an ad-hoc token on calls to any endpoint, overriding the default token stored within the OcClient instance. However, you can't really get a token in an ad-hoc way. When you call AuthenticateAsync, you're changing the default token used on the OcClient, making it hard to get different tokens using the same instance in a thread-safe way.

Don't throw on certain 404/409 responses

Methods that Get, Patch, or Delete a thing by ID throw an exception on a 404 response (object does not exist). But in some cases (arguably most of them), this 404 is a convenient existence check in your logic flow and saves you an extra API call. In other words, it's not "exceptional", so perhaps it shouldn't throw. In the case of Get and Patch, getting a null value back seems like a more natural way to convey this. Deletes don't generally have a response body so would need to think on how best to handle that one.

A similar argument could be made for 409 (object already exists) responses with Create methods.

Important to note that any changes here could be breaking, so it would be best to make a decision on this ahead of 1.0.

Allow use of ad-hoc access token

Useful in a few scenarios:

  • Authenticating using different credentials for different parts of the same process.
  • A 3rd-party integration that is passed a valid token for use against OrderCloud.io, rather than authenticating directly.

Add an optional accessToken arg to all API methods that map to an endpoint.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.