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:
parent
e751459b4d
commit
51134a5a8b
4 changed files with 1466 additions and 129 deletions
5
.changeset/thin-owls-beg.md
Normal file
5
.changeset/thin-owls-beg.md
Normal 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.
|
|
@ -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,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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,49 +62,88 @@ 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:
|
};
|
||||||
productLines?.map((line) => {
|
}
|
||||||
const lineTaxCalculated = line.taxCalculated ?? 0;
|
|
||||||
const lineTotalNetAmount = line.taxableAmount ?? 0;
|
|
||||||
const lineTotalGrossAmount = numbers.roundFloatToTwoDecimals(
|
|
||||||
lineTotalNetAmount + lineTaxCalculated
|
|
||||||
);
|
|
||||||
|
|
||||||
|
export function mapResponseProductLines(
|
||||||
|
transaction: TransactionModel
|
||||||
|
): CalculateTaxesResponse["lines"] {
|
||||||
|
const productLines = transaction.lines?.filter((line) => line.itemCode !== SHIPPING_ITEM_CODE);
|
||||||
|
|
||||||
|
return (
|
||||||
|
productLines?.map((line) => {
|
||||||
|
if (!line.isItemTaxable) {
|
||||||
return {
|
return {
|
||||||
total_gross_amount: lineTotalGrossAmount,
|
total_gross_amount: line.lineAmount ?? 0,
|
||||||
total_net_amount: lineTotalNetAmount,
|
total_net_amount: line.lineAmount ?? 0,
|
||||||
// todo: add tax rate
|
|
||||||
tax_rate: 0,
|
tax_rate: 0,
|
||||||
};
|
};
|
||||||
}) ?? [],
|
}
|
||||||
|
|
||||||
|
const lineTaxCalculated = line.taxCalculated ?? 0;
|
||||||
|
const lineTotalNetAmount = line.taxableAmount ?? 0;
|
||||||
|
const lineTotalGrossAmount = numbers.roundFloatToTwoDecimals(
|
||||||
|
lineTotalNetAmount + lineTaxCalculated
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
total_gross_amount: lineTotalGrossAmount,
|
||||||
|
total_net_amount: lineTotalNetAmount,
|
||||||
|
/*
|
||||||
|
* 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,
|
|
||||||
};
|
};
|
||||||
|
|
1316
apps/taxes/src/modules/avatax/maps/mocks.ts
Normal file
1316
apps/taxes/src/modules/avatax/maps/mocks.ts
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue