fix: returning 0 for line price if item is not taxable (#476)

* fix: 🐛 returning 0 for line price if item is not taxable

* build: 👷 add changeset
This commit is contained in:
Adrian Pilarczyk 2023-05-17 12:49:10 +02:00 committed by GitHub
parent e751459b4d
commit 51134a5a8b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 1466 additions and 129 deletions

View file

@ -0,0 +1,5 @@
---
"saleor-app-taxes": patch
---
Fix returning 0 for line price if Avatax returns isItemTaxable: false. This happens in, f.e., certain states in the US where there is no sales tax. After the fix, the app will fall back to the original line price.

View file

@ -1,111 +1,88 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { import {
AvataxCalculateTaxesMapPayloadArgs, SHIPPING_ITEM_CODE,
avataxCalculateTaxesMaps, mapPayloadLines,
mapResponseProductLines,
mapResponseShippingLine,
} from "./avatax-calculate-taxes-map"; } from "./avatax-calculate-taxes-map";
import { mapPayloadArgsMocks, transactionModelMocks } from "./mocks";
// * Mocked payload data, channel config and avatax config
const MOCKED_CALCULATE_TAXES_ARGS: AvataxCalculateTaxesMapPayloadArgs = {
taxBase: {
pricesEnteredWithTax: false,
currency: "PLN",
channel: {
slug: "channel-pln",
},
sourceObject: {
__typename: "Order",
user: {
id: "VXNlcjo5ZjY3ZjY0Zi1iZjY5LTQ5ZjYtYjQ4Zi1iZjY3ZjY0ZjY0ZjY=",
},
},
discounts: [],
address: {
streetAddress1: "123 Palm Grove Ln",
streetAddress2: "",
city: "LOS ANGELES",
country: {
code: "US",
},
countryArea: "CA",
postalCode: "90002",
},
shippingPrice: {
amount: 48.33,
},
lines: [
{
quantity: 3,
unitPrice: {
amount: 84,
},
totalPrice: {
amount: 252,
},
sourceLine: {
__typename: "OrderLine",
id: "T3JkZXJMaW5lOmY1NGQ1MWY2LTc1OTctNGY2OC1hNDk0LTFjYjZlYjRmOTlhMQ==",
variant: {
id: "UHJvZHVjdFZhcmlhbnQ6MzQ2",
product: {
metafield: null,
productType: {
metafield: null,
},
},
},
},
},
{
quantity: 1,
unitPrice: {
amount: 5.99,
},
totalPrice: {
amount: 5.99,
},
sourceLine: {
__typename: "OrderLine",
id: "T3JkZXJMaW5lOjU1NTFjNTFjLTM5MWQtNGI0Ny04MGU0LWVjY2Q5ZjU4MjQyNQ==",
variant: {
id: "UHJvZHVjdFZhcmlhbnQ6Mzg1",
product: {
metafield: null,
productType: {
metafield: null,
},
},
},
},
},
],
},
channel: {
providerInstanceId: "b8c29f49-7cae-4762-8458-e9a27eb83081",
enabled: false,
address: {
country: "US",
zip: "92093",
state: "CA",
city: "La Jolla",
street: "9500 Gilman Drive",
},
},
config: {
companyCode: "DEFAULT",
isAutocommit: false,
isSandbox: true,
name: "Avatax-1",
password: "password",
username: "username",
shippingTaxCode: "FR000000",
},
};
describe("avataxCalculateTaxesMaps", () => { describe("avataxCalculateTaxesMaps", () => {
describe.todo("mapResponse", () => { describe("mapResponseShippingLine", () => {
it.todo("calculation of fields"); it("when shipping line is not taxable, returns line amount", () => {
it.todo("formatting the fields"); const nonTaxableShippingLine = mapResponseShippingLine(transactionModelMocks.nonTaxable);
it.todo("rounding of numbers");
expect(nonTaxableShippingLine).toEqual({
shipping_price_gross_amount: 77.51,
shipping_price_net_amount: 77.51,
shipping_tax_rate: 0,
});
});
it("when shipping line is taxable and tax is included, returns calculated gross & net amounts", () => {
const taxableShippingLine = mapResponseShippingLine(
transactionModelMocks.taxable.taxIncluded
);
expect(taxableShippingLine).toEqual({
shipping_price_gross_amount: 77.51,
shipping_price_net_amount: 70.78,
shipping_tax_rate: 0,
});
});
it("when shipping line is taxable and tax is not included, returns calculated gross & net amounts", () => {
const taxableShippingLine = mapResponseShippingLine(
transactionModelMocks.taxable.taxNotIncluded
);
expect(taxableShippingLine).toEqual({
shipping_price_gross_amount: 84.87,
shipping_price_net_amount: 77.51,
shipping_tax_rate: 0,
});
});
});
describe("mapResponseProductLines", () => {
it("when product lines are not taxable, returns line amount", () => {
const nonTaxableProductLines = mapResponseProductLines(transactionModelMocks.nonTaxable);
expect(nonTaxableProductLines).toEqual([
{
total_gross_amount: 20,
total_net_amount: 20,
tax_rate: 0,
},
]);
});
it("when product lines are taxable and tax is included, returns calculated gross & net amounts", () => {
const taxableProductLines = mapResponseProductLines(
transactionModelMocks.taxable.taxIncluded
);
expect(taxableProductLines).toEqual([
{
total_gross_amount: 40,
total_net_amount: 36.53,
tax_rate: 0,
},
]);
});
it("when product lines are taxable and tax is not included, returns calculated gross & net amounts", () => {
const taxableProductLines = mapResponseProductLines(
transactionModelMocks.taxable.taxNotIncluded
);
expect(taxableProductLines).toEqual([
{
total_gross_amount: 43.8,
total_net_amount: 40,
tax_rate: 0,
},
]);
});
}); });
describe.todo("mapPayload", () => { describe.todo("mapPayload", () => {
it.todo("calculation of fields"); it.todo("calculation of fields");
@ -113,17 +90,17 @@ describe("avataxCalculateTaxesMaps", () => {
it.todo("rounding of numbers"); it.todo("rounding of numbers");
}); });
describe("mapLines", () => { describe("mapLines", () => {
const lines = avataxCalculateTaxesMaps.mapLines( const lines = mapPayloadLines(
MOCKED_CALCULATE_TAXES_ARGS.taxBase, mapPayloadArgsMocks.default.taxBase,
MOCKED_CALCULATE_TAXES_ARGS.config mapPayloadArgsMocks.default.config
); );
it("includes shipping as a line", () => { it("includes shipping as a line", () => {
expect(lines).toContainEqual({ expect(lines).toContainEqual({
itemCode: avataxCalculateTaxesMaps.shippingItemCode, itemCode: SHIPPING_ITEM_CODE,
quantity: 1, quantity: 1,
amount: 48.33, amount: 48.33,
taxCode: MOCKED_CALCULATE_TAXES_ARGS.config.shippingTaxCode, taxCode: mapPayloadArgsMocks.default.config.shippingTaxCode,
taxIncluded: false, taxIncluded: false,
}); });
}); });

View file

@ -14,9 +14,9 @@ import { avataxAddressFactory } from "./address-factory";
* * Shipping is a regular line item in Avatax * * Shipping is a regular line item in Avatax
* https://developer.avalara.com/avatax/dev-guide/shipping-and-handling/taxability-of-shipping-charges/ * https://developer.avalara.com/avatax/dev-guide/shipping-and-handling/taxability-of-shipping-charges/
*/ */
const SHIPPING_ITEM_CODE = "Shipping"; export const SHIPPING_ITEM_CODE = "Shipping";
function mapLines(taxBase: TaxBaseFragment, config: AvataxConfig): LineItemModel[] { export function mapPayloadLines(taxBase: TaxBaseFragment, config: AvataxConfig): LineItemModel[] {
const productLines = taxBase.lines.map((line) => ({ const productLines = taxBase.lines.map((line) => ({
amount: line.totalPrice.amount, amount: line.totalPrice.amount,
taxIncluded: taxBase.pricesEnteredWithTax, taxIncluded: taxBase.pricesEnteredWithTax,
@ -62,30 +62,60 @@ const mapPayload = (props: AvataxCalculateTaxesMapPayloadArgs): CreateTransactio
shipTo: avataxAddressFactory.fromSaleorAddress(taxBase.address!), shipTo: avataxAddressFactory.fromSaleorAddress(taxBase.address!),
}, },
currencyCode: taxBase.currency, currencyCode: taxBase.currency,
lines: mapLines(taxBase, config), lines: mapPayloadLines(taxBase, config),
date: new Date(), date: new Date(),
}, },
}; };
}; };
const mapResponse = (transaction: TransactionModel): CalculateTaxesResponse => { export function mapResponseShippingLine(
transaction: TransactionModel
): Pick<
CalculateTaxesResponse,
"shipping_price_gross_amount" | "shipping_price_net_amount" | "shipping_tax_rate"
> {
const shippingLine = transaction.lines?.find((line) => line.itemCode === SHIPPING_ITEM_CODE); const shippingLine = transaction.lines?.find((line) => line.itemCode === SHIPPING_ITEM_CODE);
const productLines = transaction.lines?.filter((line) => line.itemCode !== SHIPPING_ITEM_CODE); if (!shippingLine?.isItemTaxable) {
return {
shipping_price_gross_amount: shippingLine?.lineAmount ?? 0,
shipping_price_net_amount: shippingLine?.lineAmount ?? 0,
/*
* avatax doesnt return combined tax rate
* // todo: calculate percentage tax rate
*/
shipping_tax_rate: 0,
};
}
const shippingTaxCalculated = shippingLine?.taxCalculated ?? 0; const shippingTaxCalculated = shippingLine?.taxCalculated ?? 0;
const shippingTaxableAmount = shippingLine?.taxableAmount ?? 0; const shippingTaxableAmount = shippingLine?.taxableAmount ?? 0;
const shippingGrossAmount = numbers.roundFloatToTwoDecimals( const shippingGrossAmount = numbers.roundFloatToTwoDecimals(
shippingTaxableAmount + shippingTaxCalculated shippingTaxableAmount + shippingTaxCalculated
); );
const shippingNetAmount = shippingGrossAmount;
return { return {
shipping_price_gross_amount: shippingGrossAmount, shipping_price_gross_amount: shippingGrossAmount,
shipping_price_net_amount: shippingNetAmount, shipping_price_net_amount: shippingTaxableAmount,
// todo: add shipping tax rate
shipping_tax_rate: 0, shipping_tax_rate: 0,
lines: };
}
export function mapResponseProductLines(
transaction: TransactionModel
): CalculateTaxesResponse["lines"] {
const productLines = transaction.lines?.filter((line) => line.itemCode !== SHIPPING_ITEM_CODE);
return (
productLines?.map((line) => { productLines?.map((line) => {
if (!line.isItemTaxable) {
return {
total_gross_amount: line.lineAmount ?? 0,
total_net_amount: line.lineAmount ?? 0,
tax_rate: 0,
};
}
const lineTaxCalculated = line.taxCalculated ?? 0; const lineTaxCalculated = line.taxCalculated ?? 0;
const lineTotalNetAmount = line.taxableAmount ?? 0; const lineTotalNetAmount = line.taxableAmount ?? 0;
const lineTotalGrossAmount = numbers.roundFloatToTwoDecimals( const lineTotalGrossAmount = numbers.roundFloatToTwoDecimals(
@ -95,16 +125,25 @@ const mapResponse = (transaction: TransactionModel): CalculateTaxesResponse => {
return { return {
total_gross_amount: lineTotalGrossAmount, total_gross_amount: lineTotalGrossAmount,
total_net_amount: lineTotalNetAmount, total_net_amount: lineTotalNetAmount,
// todo: add tax rate /*
tax_rate: 0, * avatax doesnt return combined tax rate
* // todo: calculate percentage tax rate
*/ tax_rate: 0,
}; };
}) ?? [], }) ?? []
);
}
const mapResponse = (transaction: TransactionModel): CalculateTaxesResponse => {
const shipping = mapResponseShippingLine(transaction);
return {
...shipping,
lines: mapResponseProductLines(transaction),
}; };
}; };
export const avataxCalculateTaxesMaps = { export const avataxCalculateTaxesMaps = {
mapPayload, mapPayload,
mapResponse, mapResponse,
mapLines,
shippingItemCode: SHIPPING_ITEM_CODE,
}; };

File diff suppressed because it is too large Load diff