Skip to content

Commit

Permalink
Merge pull request #59 from oliverheywood451/tax-integrations
Browse files Browse the repository at this point in the history
Add Vertex, TaxJar, Avalara
  • Loading branch information
oliverheywood451 authored Feb 15, 2022
2 parents 34bf954 + 4146a6e commit 0e366de
Show file tree
Hide file tree
Showing 132 changed files with 3,393 additions and 87 deletions.
70 changes: 70 additions & 0 deletions OrderCloud.Catalyst.Tax.Avalara/AvalaraClient.cs
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);
}
}
}
}
35 changes: 35 additions & 0 deletions OrderCloud.Catalyst.Tax.Avalara/AvalaraCommand.cs
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)
};
}
}
}
19 changes: 19 additions & 0 deletions OrderCloud.Catalyst.Tax.Avalara/AvalaraConfig.cs
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 OrderCloud.Catalyst.Tax.Avalara/Mappers/AvalaraRequestMapper.cs
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 OrderCloud.Catalyst.Tax.Avalara/Mappers/AvalaraResponseMapper.cs
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 OrderCloud.Catalyst.Tax.Avalara/Mappers/AvalaraTaxCodeMapper.cs
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 OrderCloud.Catalyst.Tax.Avalara/Models/AvalaraAddressesModel.cs
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; }
}
}
Loading

0 comments on commit 0e366de

Please sign in to comment.