-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #59 from oliverheywood451/tax-integrations
Add Vertex, TaxJar, Avalara
- Loading branch information
Showing
132 changed files
with
3,393 additions
and
87 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
using Flurl.Http; | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Net; | ||
using System.Text; | ||
using System.Threading.Tasks; | ||
|
||
namespace OrderCloud.Catalyst.Tax.Avalara | ||
{ | ||
public class AvalaraClient | ||
{ | ||
/// <summary> | ||
/// https://developer.avalara.com/api-reference/avatax/rest/v2/methods/Definitions/ListTaxCodes/ | ||
/// </summary> | ||
public static async Task<List<AvalaraTaxCode>> ListTaxCodesAsync(string filterParam, AvalaraConfig config) | ||
{ | ||
|
||
return await TryCatchRequestAsync(config, async (request) => | ||
{ | ||
var tax = await request | ||
.AppendPathSegments("api", "v2", "definitions", "taxcodes") | ||
.SetQueryParam("$filter", filterParam) | ||
.GetJsonAsync<AvalaraFetchResult<AvalaraTaxCode>>(); | ||
return tax.value; | ||
}); | ||
} | ||
|
||
/// <summary> | ||
/// https://developer.avalara.com/api-reference/avatax/rest/v2/methods/Transactions/CreateTransaction/ | ||
/// </summary> | ||
public static async Task<AvalaraTransactionModel> CreateTransaction(AvalaraCreateTransactionModel transaction, AvalaraConfig config) | ||
{ | ||
return await TryCatchRequestAsync(config, async (request) => | ||
{ | ||
var tax = await request | ||
.AppendPathSegments("api", "v2", "transactions", "create") | ||
.PostJsonAsync(transaction).ReceiveJson<AvalaraTransactionModel>(); | ||
return tax; | ||
}); | ||
} | ||
|
||
protected static async Task<T> TryCatchRequestAsync<T>(AvalaraConfig config, Func<IFlurlRequest, Task<T>> run) | ||
{ | ||
var request = config.BaseUrl.WithBasicAuth(config.AccountID, config.LicenseKey); | ||
try | ||
{ | ||
return await run(request); | ||
} | ||
catch (FlurlHttpTimeoutException ex) // simulate with this https://stackoverflow.com/questions/100841/artificially-create-a-connection-timeout-error | ||
{ | ||
// candidate for retry here? | ||
throw new IntegrationNoResponseException(config, request.Url); | ||
} | ||
catch (FlurlHttpException ex) | ||
{ | ||
var status = ex?.Call?.Response?.StatusCode; | ||
if (status == null) // simulate by putting laptop on airplane mode | ||
{ | ||
throw new IntegrationNoResponseException(config, request.Url); | ||
} | ||
if (status == 401) | ||
{ | ||
throw new IntegrationAuthFailedException(config, request.Url, (int)status); | ||
} | ||
var body = await ex.Call.Response.GetJsonAsync(); | ||
throw new IntegrationErrorResponseException(config, request.Url, (int)status, body); | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
using System.Threading.Tasks; | ||
|
||
namespace OrderCloud.Catalyst.Tax.Avalara | ||
{ | ||
public class AvalaraCommand : OCIntegrationCommand , ITaxCodesProvider, ITaxCalculator | ||
{ | ||
public AvalaraCommand(AvalaraConfig configDefault) : base(configDefault) { } | ||
|
||
public async Task<OrderTaxCalculation> CalculateEstimateAsync(OrderSummaryForTax orderSummary, OCIntegrationConfig configOverride = null) => | ||
await CreateTransactionAsync(AvalaraDocumentType.SalesOrder, orderSummary, configOverride); | ||
|
||
public async Task<OrderTaxCalculation> CommitTransactionAsync(OrderSummaryForTax orderSummary, OCIntegrationConfig configOverride = null) => | ||
await CreateTransactionAsync(AvalaraDocumentType.SalesInvoice, orderSummary, configOverride); | ||
|
||
protected async Task<OrderTaxCalculation> CreateTransactionAsync(AvalaraDocumentType type, OrderSummaryForTax orderSummary, OCIntegrationConfig configOverride = null) | ||
{ | ||
var config = GetValidatedConfig<AvalaraConfig>(configOverride); | ||
var createTransaction = AvalaraRequestMapper.ToAvalaraTransactionModel(orderSummary, config.CompanyCode, type); | ||
var transaction = await AvalaraClient.CreateTransaction(createTransaction, config); | ||
var calculation = AvalaraResponseMapper.ToOrderTaxCalculation(transaction); | ||
return calculation; | ||
} | ||
|
||
public async Task<TaxCategorizationResponse> ListTaxCodesAsync(string filterTerm, OCIntegrationConfig configOverride = null) | ||
{ | ||
var config = GetValidatedConfig<AvalaraConfig>(configOverride); | ||
var filter = AvalaraTaxCodeMapper.MapFilterTerm(filterTerm); | ||
var codes = await AvalaraClient.ListTaxCodesAsync(filter, config); | ||
return new TaxCategorizationResponse() | ||
{ | ||
Categories = AvalaraTaxCodeMapper.MapTaxCodes(codes) | ||
}; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Text; | ||
|
||
namespace OrderCloud.Catalyst.Tax.Avalara | ||
{ | ||
public class AvalaraConfig: OCIntegrationConfig | ||
{ | ||
public override string ServiceName { get; } = "Avalara"; | ||
[RequiredIntegrationField] | ||
public string BaseUrl { get; set; } | ||
[RequiredIntegrationField] | ||
public string AccountID { get; set; } | ||
[RequiredIntegrationField] | ||
public string LicenseKey { get; set; } | ||
[RequiredIntegrationField] | ||
public string CompanyCode { get; set; } | ||
} | ||
} |
84 changes: 84 additions & 0 deletions
84
OrderCloud.Catalyst.Tax.Avalara/Mappers/AvalaraRequestMapper.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
using OrderCloud.SDK; | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Text; | ||
|
||
namespace OrderCloud.Catalyst.Tax.Avalara | ||
{ | ||
public static class AvalaraRequestMapper | ||
{ | ||
public static AvalaraCreateTransactionModel ToAvalaraTransactionModel(OrderSummaryForTax order, string companyCode, AvalaraDocumentType docType) | ||
{ | ||
var shippingLines = order.ShipEstimates.Select(ToLineItemModel); | ||
var productLines = order.LineItems.Select(ToLineItemModel); | ||
return new AvalaraCreateTransactionModel() | ||
{ | ||
companyCode = companyCode, | ||
type = docType, | ||
customerCode = order.CustomerCode, | ||
date = DateTime.Now, | ||
discount = GetOrderOnlyTotalDiscount(order), | ||
lines = productLines.Concat(shippingLines).ToList(), | ||
purchaseOrderNo = order.OrderID | ||
}; | ||
} | ||
|
||
private static AvalaraLineItemModel ToLineItemModel(LineItemSummaryForTax lineItem) | ||
{ | ||
return new AvalaraLineItemModel() | ||
{ | ||
amount = lineItem.LineTotal, // Total after line-item level promotions have been applied | ||
quantity = lineItem.Quantity, | ||
taxCode = lineItem.TaxCode, | ||
itemCode = lineItem.ProductID, | ||
discounted = true, // Assumption that all products are eligible for order-level promotions | ||
customerUsageType = null, | ||
number = lineItem.LineItemID, | ||
addresses = ToAddressesModel(lineItem.ShipFrom, lineItem.ShipTo) | ||
}; | ||
} | ||
|
||
private static AvalaraLineItemModel ToLineItemModel(ShipEstimateSummaryForTax shipEstimate) | ||
{ | ||
return new AvalaraLineItemModel() | ||
{ | ||
amount = shipEstimate.Cost, | ||
taxCode = "FR", | ||
itemCode = shipEstimate.Description, | ||
customerUsageType = null, | ||
number = shipEstimate.ShipEstimateID, | ||
addresses = ToAddressesModel(shipEstimate.ShipFrom, shipEstimate.ShipTo) | ||
}; | ||
} | ||
|
||
private static decimal GetOrderOnlyTotalDiscount(OrderSummaryForTax order) | ||
{ | ||
var sumOfLineItemLevelDiscounts = order.LineItems.Sum(li => li.PromotionDiscount); | ||
var orderLevelDiscount = order.PromotionDiscount - sumOfLineItemLevelDiscounts; | ||
return orderLevelDiscount; | ||
} | ||
|
||
private static AvalaraAddressesModel ToAddressesModel(Address shipFrom, Address shipTo) | ||
{ | ||
return new AvalaraAddressesModel() | ||
{ | ||
shipFrom = shipFrom.ToAddressLocationInfo(), | ||
shipTo = shipTo.ToAddressLocationInfo(), | ||
}; | ||
} | ||
|
||
private static AvalaraAddressLocationInfo ToAddressLocationInfo(this Address address) | ||
{ | ||
return new AvalaraAddressLocationInfo() | ||
{ | ||
line1 = address.Street1, | ||
line2 = address.Street2, | ||
city = address.City, | ||
region = address.State, | ||
postalCode = address.Zip, | ||
country = address.Country | ||
}; | ||
} | ||
} | ||
} |
53 changes: 53 additions & 0 deletions
53
OrderCloud.Catalyst.Tax.Avalara/Mappers/AvalaraResponseMapper.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Text; | ||
|
||
namespace OrderCloud.Catalyst.Tax.Avalara | ||
{ | ||
public static class AvalaraResponseMapper | ||
{ | ||
public static OrderTaxCalculation ToOrderTaxCalculation(AvalaraTransactionModel avalaraTransaction) | ||
{ | ||
var shippingLines = avalaraTransaction.lines?.Where(line => line.taxCode == "FR") ?? new List<AvalaraTransactionLineModel>(); | ||
var itemLines = avalaraTransaction.lines?.Where(line => line.taxCode != "FR") ?? new List<AvalaraTransactionLineModel>(); | ||
return new OrderTaxCalculation() | ||
{ | ||
OrderID = avalaraTransaction.purchaseOrderNo, | ||
ExternalTransactionID = avalaraTransaction.code, | ||
TotalTax = avalaraTransaction.totalTax ?? 0, | ||
LineItems = itemLines.Select(ToItemTaxDetails).ToList(), | ||
OrderLevelTaxes = shippingLines.SelectMany(ToShippingTaxDetails).ToList() | ||
}; | ||
} | ||
|
||
public static IEnumerable<TaxDetails> ToShippingTaxDetails(AvalaraTransactionLineModel transactionLineModel) | ||
{ | ||
return transactionLineModel.details?.Select(detail => ToTaxDetails(detail, transactionLineModel.lineNumber)) ?? new List<TaxDetails>(); | ||
} | ||
|
||
public static LineItemTaxCalculation ToItemTaxDetails(AvalaraTransactionLineModel transactionLineModel) | ||
{ | ||
return new LineItemTaxCalculation() | ||
{ | ||
LineItemID = transactionLineModel.lineNumber, | ||
LineItemTotalTax = transactionLineModel.taxCalculated ?? 0, | ||
LineItemLevelTaxes = transactionLineModel.details?.Select(detail => ToTaxDetails(detail, null)).ToList() ?? new List<TaxDetails>() | ||
}; | ||
} | ||
|
||
public static TaxDetails ToTaxDetails(AvalaraTransactionLineDetailModel detail, string shipEstimateID) | ||
{ | ||
return new TaxDetails() | ||
{ | ||
Tax = detail.tax ?? 0, | ||
Taxable = detail.taxableAmount ?? 0, | ||
Exempt = detail.exemptAmount ?? 0, | ||
TaxDescription = detail.taxName, | ||
JurisdictionLevel = detail.jurisdictionType.ToString(), | ||
JurisdictionValue = detail.jurisName, | ||
ShipEstimateID = shipEstimateID | ||
}; | ||
} | ||
} | ||
} |
36 changes: 36 additions & 0 deletions
36
OrderCloud.Catalyst.Tax.Avalara/Mappers/AvalaraTaxCodeMapper.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
using OrderCloud.SDK; | ||
using System; | ||
using System.Linq; | ||
using OrderCloud.Catalyst; | ||
using System.Collections.Generic; | ||
|
||
namespace OrderCloud.Catalyst.Tax.Avalara | ||
{ | ||
public static class AvalaraTaxCodeMapper | ||
{ | ||
// Tax Codes for lines on Transactions | ||
public static List<TaxCategorization> MapTaxCodes(List<AvalaraTaxCode> codes) | ||
{ | ||
return codes.Select(MapTaxCode).ToList(); | ||
} | ||
|
||
public static TaxCategorization MapTaxCode(AvalaraTaxCode code) | ||
{ | ||
return new TaxCategorization | ||
{ | ||
Code = code.taxCode, | ||
Description = code.description | ||
}; | ||
} | ||
|
||
public static string MapFilterTerm(string searchTerm) | ||
{ | ||
var searchString = $"isActive eq true"; | ||
if (searchTerm != "") | ||
{ | ||
searchString = $"{searchString} and (taxCode contains '{searchTerm}' OR description contains '{searchTerm}')"; | ||
} | ||
return searchString; | ||
} | ||
} | ||
} |
29 changes: 29 additions & 0 deletions
29
OrderCloud.Catalyst.Tax.Avalara/Models/AvalaraAddressesModel.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Text; | ||
|
||
namespace OrderCloud.Catalyst.Tax.Avalara | ||
{ | ||
public class AvalaraAddressesModel | ||
{ | ||
public AvalaraAddressLocationInfo singleLocation { get; set; } | ||
public AvalaraAddressLocationInfo shipFrom { get; set; } | ||
public AvalaraAddressLocationInfo shipTo { get; set; } | ||
public AvalaraAddressLocationInfo pointOfOrderOrigin { get; set; } | ||
public AvalaraAddressLocationInfo pointOfOrderAcceptance { get; set; } | ||
} | ||
|
||
public class AvalaraAddressLocationInfo | ||
{ | ||
public string locationCode { get; set; } | ||
public string line1 { get; set; } | ||
public string line2 { get; set; } | ||
public string line3 { get; set; } | ||
public string city { get; set; } | ||
public string region { get; set; } | ||
public string country { get; set; } | ||
public string postalCode { get; set; } | ||
public decimal? latitude { get; set; } | ||
public decimal? longitude { get; set; } | ||
} | ||
} |
Oops, something went wrong.