Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Vertex, TaxJar, Avalara #59

Merged
merged 43 commits into from
Feb 15, 2022
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
f9d0f92
adding vertex and writing a contributing guide
oliverheywood451 Jan 26, 2022
e0a6b18
update readme and folder structure
oliverheywood451 Jan 26, 2022
bfd45b6
wording tweak
oliverheywood451 Jan 26, 2022
281ce1f
wording
oliverheywood451 Jan 26, 2022
591f0f6
more tweaks
oliverheywood451 Jan 26, 2022
f987d94
last tweaks
oliverheywood451 Jan 26, 2022
2eddce5
more detail about tests
oliverheywood451 Jan 26, 2022
6fa6de2
wording
oliverheywood451 Jan 26, 2022
cc92cb6
Update README.md
oliverheywood451 Jan 26, 2022
3ea532d
Update README.md
oliverheywood451 Jan 26, 2022
405fb34
Update README.md
oliverheywood451 Jan 26, 2022
40d4e49
Update README.md
oliverheywood451 Jan 26, 2022
551254a
Update README.md
oliverheywood451 Jan 26, 2022
0ad08c7
Update README.md
oliverheywood451 Jan 26, 2022
ae2d96b
Update README.md
oliverheywood451 Jan 26, 2022
25a89df
Update README.md
oliverheywood451 Jan 26, 2022
0f14441
Typos on Integrations README
crhistianramirez Jan 26, 2022
0bf6eec
Merge pull request #1 from crhistianramirez/patch-2
oliverheywood451 Jan 26, 2022
81db3b9
Update README.md
oliverheywood451 Jan 26, 2022
432abdf
remove duplicates
oliverheywood451 Jan 26, 2022
4e4e713
Merge branch 'tax-integrations' of https://github.com/oliverheywood45…
oliverheywood451 Jan 26, 2022
266bde9
error handling is solid. now need to do tests
oliverheywood451 Jan 31, 2022
6263c51
couple readme updates
oliverheywood451 Jan 31, 2022
553d5a1
solid progress on the testing framework
oliverheywood451 Feb 1, 2022
03c56a6
more tests
oliverheywood451 Feb 1, 2022
ae11694
readme updates
oliverheywood451 Feb 1, 2022
df73caa
dont actually want this
oliverheywood451 Feb 1, 2022
bbd8aa4
Merge branch 'dev' of https://github.com/ordercloud-api/ordercloud-do…
oliverheywood451 Feb 1, 2022
995b14c
switch exception type from int to HttpStatusCode
oliverheywood451 Feb 7, 2022
1848632
change double to decimal
oliverheywood451 Feb 7, 2022
d214766
putting shipping code into a const
oliverheywood451 Feb 7, 2022
15e7509
update readme
oliverheywood451 Feb 7, 2022
56023ea
taxjar is working. still needs tests and error handling.
oliverheywood451 Feb 7, 2022
0567b83
vertex looking pretty good
oliverheywood451 Feb 8, 2022
080dd6d
refactored Interface to not rely on OrderWorksheet. Instead, there's …
oliverheywood451 Feb 8, 2022
eaae6f5
units tests are back. nice!
oliverheywood451 Feb 8, 2022
b07c270
Avalara looks good! still needs tests and readme
oliverheywood451 Feb 9, 2022
7f53c36
updated to support config overrides. This is important if different s…
oliverheywood451 Feb 11, 2022
4b1ada0
move to separate projects
oliverheywood451 Feb 11, 2022
59bf246
updating readmes for project structure
oliverheywood451 Feb 11, 2022
aa73883
remove this as were not ready to be that public
oliverheywood451 Feb 11, 2022
094d5cf
readme updates
oliverheywood451 Feb 11, 2022
4146a6e
readme and package description updates
oliverheywood451 Feb 14, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ If you're building solutions for OrderCloud using .NET and find a particular tas

## Features

### [Commerce Integration List](./library/OrderCLoud.Catalyst/Integrations/Implementations/README.md)

Interact with popular 3rd party APIs that provide functionality useful for commerce. Integrations within a category are made interoperable with an interface.

[Guide to adding an Integration](./library/OrderCLoud.Catalyst/Integrations/README.md)

### [User Authentication](https://github.com/ordercloud-api/ordercloud-dotnet-catalyst/tree/dev/library/OrderCloud.Catalyst/Auth/UserAuth)

Use Ordercloud's authentication scheme in your own APIs.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace OrderCloud.Catalyst
{
public class IntegrationAuthFailedException : CatalystBaseException
{
public IntegrationAuthFailedException(OCIntegrationConfig config, string requestUrl) : base(
"IntegrationAuthorizationFailed",
$"Authentication to 3rd party service \"{config.ServiceName}\" failed. Check your config credentials.",
new IntegrationAuthFailedError()
{
ServiceName = config.ServiceName,
RequestUrl = requestUrl,
},
400) {}
oliverheywood451 marked this conversation as resolved.
Show resolved Hide resolved
}

public class IntegrationAuthFailedError
{
public string ServiceName { get; set; }
public string RequestUrl { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System;
oliverheywood451 marked this conversation as resolved.
Show resolved Hide resolved
using System.Collections.Generic;
using System.Text;

namespace OrderCloud.Catalyst
{
public class IntegrationErrorResponseException : CatalystBaseException
{
public IntegrationErrorResponseException(OCIntegrationConfig config, string requestUrl, object responseBody) : base(
"IntegrationErrorResponse",
$"Request to 3rd party service \"{config.ServiceName}\" resulted in an error. See body for details.",
new IntegrationErrorResponseError()
{
ServiceName = config.ServiceName,
RequestUrl = requestUrl,
ResponseBody = responseBody
}
, 400)
{ }
}

public class IntegrationErrorResponseError
{
public string ServiceName { get; set; }
public string RequestUrl { get; set; }
public object ResponseBody { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace OrderCloud.Catalyst
{
public class IntegrationMissingConfigsException : CatalystBaseException
{
public IntegrationMissingConfigsException(OCIntegrationConfig config, List<string> missingFields) : base(
"IntegrationMissingConfigs",
$"Configuration field(s) for 3rd party service \"{config.ServiceName}\" are null or empty. Check fields on class {config.GetType().Name}.",
new IntegrationMissingConfigs()
{
ServiceName = config.ServiceName,
MissingFieldNames = missingFields
},
400) { }
}

public class IntegrationMissingConfigs
{
public string ServiceName { get; set; }
public List<string> MissingFieldNames { get; set; } = new List<string>();
}

[AttributeUsage(AttributeTargets.Property)]
public class RequiredIntegrationFieldAttribute : Attribute
{

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System;
oliverheywood451 marked this conversation as resolved.
Show resolved Hide resolved
using System.Collections.Generic;
using System.Text;

namespace OrderCloud.Catalyst
{
public class IntegrationNoResponseException : CatalystBaseException
{
public IntegrationNoResponseException(OCIntegrationConfig config, string requestUrl) : base(
"IntegrationNoResponse",
$"Request to 3rd party service \"{config.ServiceName}\" returned no response.",
new IntegrationNoResponseError()
{
ServiceName = config.ServiceName,
RequestUrl = requestUrl,
}
, 400)
{ }
}

public class IntegrationNoResponseError
{
public string ServiceName { get; set; }
public string RequestUrl { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# List of Integrations

| Name | Website | Integration Guide | Contributed By | Categories |
| ------------- | ------------- | ------------- | ------------- | ------------- |
| Vertex | https://www.vertexinc.com/ | [./Vertex/README.md](./Vertex/README.md) | OrderCloud Team | Tax
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
using OrderCloud.SDK;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;

namespace OrderCloud.Catalyst
{
public static class VertexRequestMapper
{
public static VertexCalculateTaxRequest ToVertexCalculateTaxRequest(this OrderWorksheet order, List<OrderPromotion> promosOnOrder, string companyCode, VertexSaleMessageType type)
oliverheywood451 marked this conversation as resolved.
Show resolved Hide resolved
{
var itemLines = order.LineItems.Select(li => ToVertexLineItem(li));
var shippingLines = order.ShipEstimateResponse.ShipEstimates.Select(se =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think of avoiding using the var? Because it's not clear for reviewers which type is it and decreases the speed of reading and understanding the code. The IEnumerable gives more information in this case :)

Copy link
Collaborator Author

@oliverheywood451 oliverheywood451 Feb 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using var is a pattern that's pretty engrained in me from all the C# projects at four51. I do see how it makes reviewing code hard if you're not viewing in the VS editor (where hovering shows you the type).

I'm willing to consider this. It would be a rather large code change throughout the project. Do you know what the OC platform code does? That's often what we model these other tools on.

Copy link
Contributor

@crhistianramirez crhistianramirez Feb 9, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My personal opinion is to use it only if the type can't be easily inferred by its context/usage. Adds a lot of clutter which in my opinion actually makes it harder to read (more mental load) and is unnecessarily verbose in many situations

ie:

Car car = new Car();

{
var firstLi = order.LineItems.FirstOrDefault(li => li.ID == se.ShipEstimateItems.First().LineItemID);
Require.That(firstLi != null, new CatalystBaseException("InvalidOrderWorksheet", $"Invalid OrderWorksheet. Based on ShipEstimateItems, expected to find a LineItem with ID {se.ShipEstimateItems.First().LineItemID}", null, 400));
return ToVertexLineItem(se, firstLi.ShippingAddress);
});

return new VertexCalculateTaxRequest()
{
postingDate = DateTime.Now.ToString("yyyy-MM-dd"),
oliverheywood451 marked this conversation as resolved.
Show resolved Hide resolved
saleMessageType = type.ToString(),
transactionType = VertexTransactionType.SALE.ToString(),
transactionId = order.Order.ID,
seller = new VertexSeller()
{
company = companyCode
},
customer = new VertexCustomer()
{
customerCode = new VertexCustomerCode()
{
classCode = order.Order.FromUserID,
value = order.Order.FromUser.Email
},
},
lineItems = itemLines.Concat(shippingLines).ToList()
};
}

public static VertexLineItem ToVertexLineItem(LineItem lineItem)
{
return new VertexLineItem()
{
customer = new VertexCustomer()
{
destination = lineItem.ShippingAddress.ToVertexLocation(),
},
product = new VertexProduct()
{
productClass = lineItem.Product.ID,
value = lineItem.Product.Name
},
quantity = new VertexMeasure()
{
value = lineItem.Quantity
},
unitPrice = (double) lineItem.UnitPrice,
oliverheywood451 marked this conversation as resolved.
Show resolved Hide resolved
lineItemId = lineItem.ID,
extendedPrice = (double) lineItem.LineTotal // this takes precedence over quanitity and unit price in determining tax cost
};
}

public static VertexLineItem ToVertexLineItem(ShipEstimate shipEstimate, Address shipTo)
{
var selectedMethod = shipEstimate.ShipMethods.FirstOrDefault(m => m.ID == shipEstimate.SelectedShipMethodID);
Require.That(selectedMethod != null, new CatalystBaseException("InvalidOrderWorksheet", $"Invalid OrderWorksheet. Based on SelectedShipMethodID, expected to find a ShipMethod with ID {shipEstimate.SelectedShipMethodID}", null, 400));
return new VertexLineItem()
{
customer = new VertexCustomer()
{
destination = shipTo.ToVertexLocation(),
},
product = new VertexProduct()
{
productClass = "shipping_code",
oliverheywood451 marked this conversation as resolved.
Show resolved Hide resolved
value = selectedMethod.Name
},
quantity = new VertexMeasure()
{
value = 1
},
unitPrice = (double) selectedMethod.Cost,
lineItemId = shipEstimate.ID,
};
}

public static VertexLocation ToVertexLocation(this Address address)
{
return new VertexLocation()
{
streetAddress1 = address.Street1,
streetAddress2 = address.Street2,
city = address.City,
mainDivision = address.State,
postalCode = address.Zip,
country = address.Country
};
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace OrderCloud.Catalyst
{
public static class VertexResponseMapper
{
public static bool IsShippingLineItem(this VertexResponseLineItem li) => li.product.productClass == "shipping_code";

public static OrderTaxCalculation ToOrderTaxCalculation(this VertexCalculateTaxResponse response)
{
var shippingLines = response.lineItems?.Where(li => li.IsShippingLineItem()) ?? new List<VertexResponseLineItem>();
var itemLines = response.lineItems?.Where(li => !li.IsShippingLineItem()) ?? new List<VertexResponseLineItem>();

return new OrderTaxCalculation()
{
OrderID = response.transactionId,
ExternalTransactionID = response.transactionId,
TotalTax = (decimal) response.totalTax,
LineItems = itemLines.Select(ToItemTaxDetails).ToList(),
OrderLevelTaxes = shippingLines.SelectMany(ToShippingTaxDetails).ToList()
};
}

public static IEnumerable<TaxDetails> ToShippingTaxDetails(this VertexResponseLineItem transactionLineModel)
{
return transactionLineModel.taxes?.Select(detail => detail.ToTaxDetails(transactionLineModel.lineItemId)) ?? new List<TaxDetails>();
}

public static LineItemTaxCalculation ToItemTaxDetails(this VertexResponseLineItem transactionLineModel)
{
return new LineItemTaxCalculation()
{
LineItemID = transactionLineModel.lineItemId,
LineItemTotalTax = (decimal) transactionLineModel.totalTax,
LineItemLevelTaxes = transactionLineModel.taxes?.Select(detail => detail.ToTaxDetails(null)).ToList() ?? new List<TaxDetails>()
};
}

public static TaxDetails ToTaxDetails(this VertexTax detail, string shipEstimateID)
{
return new TaxDetails()
{
Tax = (decimal) detail.calculatedTax,
Taxable = (decimal) detail.taxable,
Exempt = 0, // we don't get a property back for exempt
TaxDescription = detail.impositionType.value,
JurisdictionLevel = detail.jurisdiction.jurisdictionLevel.ToString(),
JurisdictionValue = detail.jurisdiction.value,
ShipEstimateID = shipEstimateID
};
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace OrderCloud.Catalyst
{
public class VertexCalculateTaxRequest
{
public string saleMessageType { get; set; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious, why do we have the public properties which start with a lower case? Do we have a code notation standard for the OC Catalyst?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are lowercase because when they are serialized to JSON they need to be lower case to match the property names the Vertex API is expecting.

We don't have a code notation standard for the OC Catalyst - I can create an issue for that. If you want to work on it that would be awesome.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#60

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we really cared, we could use JsonProperty to determine how something is serialized. For example

[JsonProperty("saleMessageType")]
public string SaleMessageType {get; set;}

But it seems like an abstraction just for the sake of abstraction. I personally like that the property names match exactly what the vertex properties are.

public VertexSeller seller { get; set; }
public VertexCustomer customer { get; set; }
public VertexDiscount discount { get; set; }
public List<VertexLineItem> lineItems { get; set; } = new List<VertexLineItem>();
public string postingDate { get; set; }
public string transactionId { get; set; }
public string transactionType { get; set; }
}

public class VertexSitusOverride
{
public VertexTaxingLocation taxingLocation { get; set; }
}


public enum VertexTaxingLocation
{
ADMINISTRATIVE_DESTINATION, ADMINISTRATIVE_ORIGIN, DESTINATION, PHYSICAL_ORIGIN
}

public class VertexImposition
{
public string impositionType { get; set; }
public VertexJurisdictionLevel value { get; set; }
}


public enum VertexTaxOverrideType
{
TAXABLE, NONTAXABLE
}

public class VertexTaxOverride
{
public VertexTaxOverrideType overrideType { get; set; }
public string overrideReasonCode { get; set; }
}

public enum VertexSaleMessageType
{
QUOTATION,
INVOICE,
DISTRIBUTE_TAX
}

public class VertexTaxRegistrations
{
public string taxRegistrationNumber { get; set; }
public string isoCountryCode { get; set; }
public string mainDivision { get; set; }
public string hasPhysicalPresenceIndicator { get; set; }
public string impositionType { get; set; }
}


public enum VertexLocationCustomsStatus
{
FREE_CIRCULATION, BONDED_WAREHOUSE, FREE_TRADE_ZONE, TEMPORARY_IMPORT, INWARD_PROCESSING_RELIEF, OUTWARD_PROCESSING_RELIEF
}

public class VertexExemptionCertificate
{
public string exemptionCertificateNumber { get; set; }
public string value { get; set; }
}
}
Loading