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 all 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
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