feat: tax code create order (#693)
* feat: ✨ add avatax-order-created-tax-code-matcher && extract calculate-taxes matcher * refactor: 🚚 TaxJarTaxCodeMatcher -> TaxJarCalculateTaxesTaxCodeMatcher * feat: ✨ add taxjar-order-created-payload-service with tax code * feat: ✅ add missing tests * build: 👷 add changeset
This commit is contained in:
parent
47102ba98c
commit
d2b21cc1ab
27 changed files with 512 additions and 169 deletions
5
.changeset/light-rocks-count.md
Normal file
5
.changeset/light-rocks-count.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
"saleor-app-taxes": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Added the usage of stored tax code combinations in the create order webhook flow. This doesn't effect the tax calculation, but makes sure the mapped product line has the correct tax code.
|
|
@ -2,6 +2,9 @@ fragment OrderLine on OrderLine {
|
||||||
productSku
|
productSku
|
||||||
productName
|
productName
|
||||||
quantity
|
quantity
|
||||||
|
taxClass {
|
||||||
|
id
|
||||||
|
}
|
||||||
unitPrice {
|
unitPrice {
|
||||||
net {
|
net {
|
||||||
amount
|
amount
|
||||||
|
|
|
@ -2,6 +2,9 @@ fragment OrderLine on OrderLine {
|
||||||
productSku
|
productSku
|
||||||
productName
|
productName
|
||||||
quantity
|
quantity
|
||||||
|
taxClass {
|
||||||
|
id
|
||||||
|
}
|
||||||
unitPrice {
|
unitPrice {
|
||||||
net {
|
net {
|
||||||
amount
|
amount
|
||||||
|
|
|
@ -36,7 +36,7 @@ export class AvataxWebhookService implements ProviderWebhookService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOrder(order: OrderCreatedSubscriptionFragment) {
|
async createOrder(order: OrderCreatedSubscriptionFragment) {
|
||||||
const adapter = new AvataxOrderCreatedAdapter(this.config);
|
const adapter = new AvataxOrderCreatedAdapter(this.config, this.authData);
|
||||||
|
|
||||||
const response = await adapter.send({ order });
|
const response = await adapter.send({ order });
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { LineItemModel } from "avatax/lib/models/LineItemModel";
|
||||||
import { TaxBaseFragment } from "../../../../generated/graphql";
|
import { TaxBaseFragment } from "../../../../generated/graphql";
|
||||||
import { AvataxConfig } from "../avatax-connection-schema";
|
import { AvataxConfig } from "../avatax-connection-schema";
|
||||||
import { AvataxTaxCodeMatches } from "../tax-code/avatax-tax-code-match-repository";
|
import { AvataxTaxCodeMatches } from "../tax-code/avatax-tax-code-match-repository";
|
||||||
import { AvataxTaxCodeMatcher } from "../tax-code/avatax-tax-code-matcher";
|
import { AvataxCalculateTaxesTaxCodeMatcher } from "./avatax-calculate-taxes-tax-code-matcher";
|
||||||
import { SHIPPING_ITEM_CODE } from "./avatax-calculate-taxes-adapter";
|
import { SHIPPING_ITEM_CODE } from "./avatax-calculate-taxes-adapter";
|
||||||
|
|
||||||
export class AvataxCalculateTaxesPayloadLinesTransformer {
|
export class AvataxCalculateTaxesPayloadLinesTransformer {
|
||||||
|
@ -13,7 +13,7 @@ export class AvataxCalculateTaxesPayloadLinesTransformer {
|
||||||
): LineItemModel[] {
|
): LineItemModel[] {
|
||||||
const isDiscounted = taxBase.discounts.length > 0;
|
const isDiscounted = taxBase.discounts.length > 0;
|
||||||
const productLines: LineItemModel[] = taxBase.lines.map((line) => {
|
const productLines: LineItemModel[] = taxBase.lines.map((line) => {
|
||||||
const matcher = new AvataxTaxCodeMatcher();
|
const matcher = new AvataxCalculateTaxesTaxCodeMatcher();
|
||||||
const taxCode = matcher.match(line, matches);
|
const taxCode = matcher.match(line, matches);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { TaxBaseLineFragment } from "../../../../generated/graphql";
|
import { TaxBaseLineFragment } from "../../../../generated/graphql";
|
||||||
import { AvataxTaxCodeMatches } from "./avatax-tax-code-match-repository";
|
import { AvataxTaxCodeMatches } from "../tax-code/avatax-tax-code-match-repository";
|
||||||
import { AvataxTaxCodeMatcher } from "./avatax-tax-code-matcher";
|
import { AvataxCalculateTaxesTaxCodeMatcher } from "./avatax-calculate-taxes-tax-code-matcher";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
const matcher = new AvataxTaxCodeMatcher();
|
const matcher = new AvataxCalculateTaxesTaxCodeMatcher();
|
||||||
|
|
||||||
describe("AvataxTaxCodeMatcher", () => {
|
describe("AvataxCalculateTaxesTaxCodeMatcher", () => {
|
||||||
it("returns empty string when tax class is not found", () => {
|
it("returns empty string when tax class is not found", () => {
|
||||||
const line: TaxBaseLineFragment = {
|
const line: TaxBaseLineFragment = {
|
||||||
quantity: 1,
|
quantity: 1,
|
|
@ -1,7 +1,7 @@
|
||||||
import { TaxBaseLineFragment } from "../../../../generated/graphql";
|
import { TaxBaseLineFragment } from "../../../../generated/graphql";
|
||||||
import { AvataxTaxCodeMatches } from "./avatax-tax-code-match-repository";
|
import { AvataxTaxCodeMatches } from "../tax-code/avatax-tax-code-match-repository";
|
||||||
|
|
||||||
export class AvataxTaxCodeMatcher {
|
export class AvataxCalculateTaxesTaxCodeMatcher {
|
||||||
private mapTaxClassWithTaxMatch(taxClassId: string, matches: AvataxTaxCodeMatches) {
|
private mapTaxClassWithTaxMatch(taxClassId: string, matches: AvataxTaxCodeMatches) {
|
||||||
return matches.find((m) => m.data.saleorTaxClassId === taxClassId);
|
return matches.find((m) => m.data.saleorTaxClassId === taxClassId);
|
||||||
}
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { AuthData } from "@saleor/app-sdk/APL";
|
||||||
import { OrderCreatedSubscriptionFragment } from "../../../../generated/graphql";
|
import { OrderCreatedSubscriptionFragment } from "../../../../generated/graphql";
|
||||||
import { Logger, createLogger } from "../../../lib/logger";
|
import { Logger, createLogger } from "../../../lib/logger";
|
||||||
import { CreateOrderResponse } from "../../taxes/tax-provider-webhook";
|
import { CreateOrderResponse } from "../../taxes/tax-provider-webhook";
|
||||||
|
@ -5,6 +6,7 @@ import { WebhookAdapter } from "../../taxes/tax-webhook-adapter";
|
||||||
import { AvataxClient } from "../avatax-client";
|
import { AvataxClient } from "../avatax-client";
|
||||||
import { AvataxConfig } from "../avatax-connection-schema";
|
import { AvataxConfig } from "../avatax-connection-schema";
|
||||||
import { AvataxOrderCreatedPayloadTransformer } from "./avatax-order-created-payload-transformer";
|
import { AvataxOrderCreatedPayloadTransformer } from "./avatax-order-created-payload-transformer";
|
||||||
|
import { AvataxOrderCreatedPayloadService } from "./avatax-order-created-payload.service";
|
||||||
import { AvataxOrderCreatedResponseTransformer } from "./avatax-order-created-response-transformer";
|
import { AvataxOrderCreatedResponseTransformer } from "./avatax-order-created-response-transformer";
|
||||||
|
|
||||||
type AvataxOrderCreatedPayload = {
|
type AvataxOrderCreatedPayload = {
|
||||||
|
@ -17,15 +19,15 @@ export class AvataxOrderCreatedAdapter
|
||||||
{
|
{
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
|
||||||
constructor(private readonly config: AvataxConfig) {
|
constructor(private readonly config: AvataxConfig, private authData: AuthData) {
|
||||||
this.logger = createLogger({ name: "AvataxOrderCreatedAdapter" });
|
this.logger = createLogger({ name: "AvataxOrderCreatedAdapter" });
|
||||||
}
|
}
|
||||||
|
|
||||||
async send(payload: AvataxOrderCreatedPayload): Promise<AvataxOrderCreatedResponse> {
|
async send(payload: AvataxOrderCreatedPayload): Promise<AvataxOrderCreatedResponse> {
|
||||||
this.logger.debug("Transforming the Saleor payload for creating order with Avatax...");
|
this.logger.debug("Transforming the Saleor payload for creating order with Avatax...");
|
||||||
|
|
||||||
const payloadTransformer = new AvataxOrderCreatedPayloadTransformer(this.config);
|
const payloadService = new AvataxOrderCreatedPayloadService(this.authData);
|
||||||
const target = payloadTransformer.transform(payload);
|
const target = await payloadService.getPayload(payload.order, this.config);
|
||||||
|
|
||||||
this.logger.debug("Calling Avatax createTransaction with transformed payload...");
|
this.logger.debug("Calling Avatax createTransaction with transformed payload...");
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { AvataxOrderCreatedPayloadLinesTransformer } from "./avatax-order-created-payload-lines-transformer";
|
||||||
|
import { avataxConfigMock } from "./avatax-order-created-payload-transformer.test";
|
||||||
|
import { AvataxTaxCodeMatches } from "../tax-code/avatax-tax-code-match-repository";
|
||||||
|
import { AvataxOrderCreatedMockGenerator } from "./avatax-order-created-mock-generator";
|
||||||
|
|
||||||
|
const linesTransformer = new AvataxOrderCreatedPayloadLinesTransformer();
|
||||||
|
const mockGenerator = new AvataxOrderCreatedMockGenerator();
|
||||||
|
const orderMock = mockGenerator.generateOrder();
|
||||||
|
|
||||||
|
const matches: AvataxTaxCodeMatches = [];
|
||||||
|
|
||||||
|
describe("AvataxOrderCreatedPayloadLinesTransformer", () => {
|
||||||
|
const lines = linesTransformer.transform(orderMock, avataxConfigMock, matches);
|
||||||
|
|
||||||
|
it("returns the correct number of lines", () => {
|
||||||
|
expect(lines).toHaveLength(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("includes shipping as a line", () => {
|
||||||
|
expect(lines).toContainEqual({
|
||||||
|
itemCode: "Shipping",
|
||||||
|
taxCode: "FR000000",
|
||||||
|
quantity: 1,
|
||||||
|
amount: 59.17,
|
||||||
|
taxIncluded: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("includes products as lines", () => {
|
||||||
|
const [first, second, third] = lines;
|
||||||
|
|
||||||
|
expect(first).toContain({
|
||||||
|
itemCode: "328223580",
|
||||||
|
description: "Monospace Tee",
|
||||||
|
quantity: 3,
|
||||||
|
amount: 65.18,
|
||||||
|
});
|
||||||
|
expect(second).toContain({
|
||||||
|
itemCode: "328223581",
|
||||||
|
description: "Monospace Tee",
|
||||||
|
quantity: 1,
|
||||||
|
amount: 21.73,
|
||||||
|
});
|
||||||
|
expect(third).toContain({
|
||||||
|
itemCode: "118223581",
|
||||||
|
description: "Paul's Balance 420",
|
||||||
|
quantity: 2,
|
||||||
|
amount: 108.63,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { LineItemModel } from "avatax/lib/models/LineItemModel";
|
||||||
|
import { OrderCreatedSubscriptionFragment } from "../../../../generated/graphql";
|
||||||
|
import { numbers } from "../../taxes/numbers";
|
||||||
|
import { AvataxConfig } from "../avatax-connection-schema";
|
||||||
|
import { AvataxTaxCodeMatches } from "../tax-code/avatax-tax-code-match-repository";
|
||||||
|
import { SHIPPING_ITEM_CODE } from "./avatax-order-created-payload-transformer";
|
||||||
|
import { AvataxOrderCreatedTaxCodeMatcher } from "./avatax-order-created-tax-code-matcher";
|
||||||
|
|
||||||
|
export class AvataxOrderCreatedPayloadLinesTransformer {
|
||||||
|
transform(
|
||||||
|
order: OrderCreatedSubscriptionFragment,
|
||||||
|
config: AvataxConfig,
|
||||||
|
matches: AvataxTaxCodeMatches
|
||||||
|
): LineItemModel[] {
|
||||||
|
const productLines: LineItemModel[] = order.lines.map((line) => {
|
||||||
|
const matcher = new AvataxOrderCreatedTaxCodeMatcher();
|
||||||
|
const taxCode = matcher.match(line, matches);
|
||||||
|
|
||||||
|
return {
|
||||||
|
// taxes are included because we treat what is passed in payload as the source of truth
|
||||||
|
taxIncluded: true,
|
||||||
|
amount: numbers.roundFloatToTwoDecimals(
|
||||||
|
line.totalPrice.net.amount + line.totalPrice.tax.amount
|
||||||
|
),
|
||||||
|
taxCode,
|
||||||
|
quantity: line.quantity,
|
||||||
|
description: line.productName,
|
||||||
|
itemCode: line.productSku ?? "",
|
||||||
|
discounted: order.discounts.length > 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (order.shippingPrice.net.amount !== 0) {
|
||||||
|
// * In Avatax, shipping is a regular line
|
||||||
|
const shippingLine: LineItemModel = {
|
||||||
|
amount: order.shippingPrice.gross.amount,
|
||||||
|
taxIncluded: true,
|
||||||
|
itemCode: SHIPPING_ITEM_CODE,
|
||||||
|
/**
|
||||||
|
* * Different shipping methods can have different tax codes.
|
||||||
|
* https://developer.avalara.com/ecommerce-integration-guide/sales-tax-badge/designing/non-standard-items/\
|
||||||
|
*/
|
||||||
|
taxCode: config.shippingTaxCode,
|
||||||
|
quantity: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
return [...productLines, shippingLine];
|
||||||
|
}
|
||||||
|
|
||||||
|
return productLines;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +1,9 @@
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { AvataxOrderCreatedMockGenerator } from "./avatax-order-created-mock-generator";
|
import { AvataxOrderCreatedMockGenerator } from "./avatax-order-created-mock-generator";
|
||||||
import {
|
import { AvataxOrderCreatedPayloadTransformer } from "./avatax-order-created-payload-transformer";
|
||||||
AvataxOrderCreatedPayloadTransformer,
|
|
||||||
mapLines,
|
|
||||||
} from "./avatax-order-created-payload-transformer";
|
|
||||||
|
|
||||||
const mockGenerator = new AvataxOrderCreatedMockGenerator();
|
const mockGenerator = new AvataxOrderCreatedMockGenerator();
|
||||||
|
|
||||||
const orderMock = mockGenerator.generateOrder();
|
const orderMock = mockGenerator.generateOrder();
|
||||||
const discountedOrderMock = mockGenerator.generateOrder({
|
const discountedOrderMock = mockGenerator.generateOrder({
|
||||||
discounts: [
|
discounts: [
|
||||||
|
@ -17,19 +15,14 @@ const discountedOrderMock = mockGenerator.generateOrder({
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
const avataxConfigMock = mockGenerator.generateAvataxConfig();
|
|
||||||
const channelConfigMock = mockGenerator.generateChannelConfig();
|
export const avataxConfigMock = mockGenerator.generateAvataxConfig();
|
||||||
|
|
||||||
describe("AvataxOrderCreatedPayloadTransformer", () => {
|
describe("AvataxOrderCreatedPayloadTransformer", () => {
|
||||||
it("returns lines with discounted: true when there are discounts", () => {
|
it("returns lines with discounted: true when there are discounts", () => {
|
||||||
const transformer = new AvataxOrderCreatedPayloadTransformer(avataxConfigMock);
|
const transformer = new AvataxOrderCreatedPayloadTransformer();
|
||||||
const payloadMock = {
|
|
||||||
order: discountedOrderMock,
|
|
||||||
providerConfig: avataxConfigMock,
|
|
||||||
channelConfig: channelConfigMock,
|
|
||||||
};
|
|
||||||
|
|
||||||
const payload = transformer.transform(payloadMock);
|
const payload = transformer.transform(discountedOrderMock, avataxConfigMock, []);
|
||||||
|
|
||||||
const linesWithoutShipping = payload.model.lines.slice(0, -1);
|
const linesWithoutShipping = payload.model.lines.slice(0, -1);
|
||||||
const check = linesWithoutShipping.every((line) => line.discounted === true);
|
const check = linesWithoutShipping.every((line) => line.discounted === true);
|
||||||
|
@ -37,14 +30,8 @@ describe("AvataxOrderCreatedPayloadTransformer", () => {
|
||||||
expect(check).toBe(true);
|
expect(check).toBe(true);
|
||||||
});
|
});
|
||||||
it("returns lines with discounted: false when there are no discounts", () => {
|
it("returns lines with discounted: false when there are no discounts", () => {
|
||||||
const transformer = new AvataxOrderCreatedPayloadTransformer(avataxConfigMock);
|
const transformer = new AvataxOrderCreatedPayloadTransformer();
|
||||||
const payloadMock = {
|
const payload = transformer.transform(orderMock, avataxConfigMock, []);
|
||||||
order: orderMock,
|
|
||||||
providerConfig: avataxConfigMock,
|
|
||||||
channelConfig: channelConfigMock,
|
|
||||||
};
|
|
||||||
|
|
||||||
const payload = transformer.transform(payloadMock);
|
|
||||||
|
|
||||||
const linesWithoutShipping = payload.model.lines.slice(0, -1);
|
const linesWithoutShipping = payload.model.lines.slice(0, -1);
|
||||||
const check = linesWithoutShipping.every((line) => line.discounted === false);
|
const check = linesWithoutShipping.every((line) => line.discounted === false);
|
||||||
|
@ -52,44 +39,3 @@ describe("AvataxOrderCreatedPayloadTransformer", () => {
|
||||||
expect(check).toBe(true);
|
expect(check).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("mapLines", () => {
|
|
||||||
const lines = mapLines(orderMock, avataxConfigMock);
|
|
||||||
|
|
||||||
it("returns the correct number of lines", () => {
|
|
||||||
expect(lines).toHaveLength(4);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("includes shipping as a line", () => {
|
|
||||||
expect(lines).toContainEqual({
|
|
||||||
itemCode: "Shipping",
|
|
||||||
taxCode: "FR000000",
|
|
||||||
quantity: 1,
|
|
||||||
amount: 59.17,
|
|
||||||
taxIncluded: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("includes products as lines", () => {
|
|
||||||
const [first, second, third] = lines;
|
|
||||||
|
|
||||||
expect(first).toContain({
|
|
||||||
itemCode: "328223580",
|
|
||||||
description: "Monospace Tee",
|
|
||||||
quantity: 3,
|
|
||||||
amount: 65.18,
|
|
||||||
});
|
|
||||||
expect(second).toContain({
|
|
||||||
itemCode: "328223581",
|
|
||||||
description: "Monospace Tee",
|
|
||||||
quantity: 1,
|
|
||||||
amount: 21.73,
|
|
||||||
});
|
|
||||||
expect(third).toContain({
|
|
||||||
itemCode: "118223581",
|
|
||||||
description: "Paul's Balance 420",
|
|
||||||
quantity: 2,
|
|
||||||
amount: 108.63,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
|
@ -1,78 +1,44 @@
|
||||||
import { LineItemModel } from "avatax/lib/models/LineItemModel";
|
|
||||||
import { OrderCreatedSubscriptionFragment } from "../../../../generated/graphql";
|
|
||||||
import { numbers } from "../../taxes/numbers";
|
|
||||||
import { AvataxConfig } from "../avatax-connection-schema";
|
|
||||||
import { avataxAddressFactory } from "../address-factory";
|
|
||||||
import { DocumentType } from "avatax/lib/enums/DocumentType";
|
import { DocumentType } from "avatax/lib/enums/DocumentType";
|
||||||
|
import { OrderCreatedSubscriptionFragment } from "../../../../generated/graphql";
|
||||||
import { discountUtils } from "../../taxes/discount-utils";
|
import { discountUtils } from "../../taxes/discount-utils";
|
||||||
|
import { avataxAddressFactory } from "../address-factory";
|
||||||
import { CreateTransactionArgs } from "../avatax-client";
|
import { CreateTransactionArgs } from "../avatax-client";
|
||||||
|
import { AvataxConfig } from "../avatax-connection-schema";
|
||||||
|
import { AvataxTaxCodeMatches } from "../tax-code/avatax-tax-code-match-repository";
|
||||||
|
import { AvataxOrderCreatedPayloadLinesTransformer } from "./avatax-order-created-payload-lines-transformer";
|
||||||
|
|
||||||
const SHIPPING_ITEM_CODE = "Shipping";
|
export const SHIPPING_ITEM_CODE = "Shipping";
|
||||||
|
|
||||||
// ? separate class?
|
|
||||||
export function mapLines(
|
|
||||||
order: OrderCreatedSubscriptionFragment,
|
|
||||||
config: AvataxConfig
|
|
||||||
): LineItemModel[] {
|
|
||||||
const productLines: LineItemModel[] = order.lines.map((line) => ({
|
|
||||||
// taxes are included because we treat what is passed in payload as the source of truth
|
|
||||||
taxIncluded: true,
|
|
||||||
amount: numbers.roundFloatToTwoDecimals(
|
|
||||||
line.totalPrice.net.amount + line.totalPrice.tax.amount
|
|
||||||
),
|
|
||||||
// todo: get from tax code matcher
|
|
||||||
taxCode: "",
|
|
||||||
quantity: line.quantity,
|
|
||||||
description: line.productName,
|
|
||||||
itemCode: line.productSku ?? "",
|
|
||||||
discounted: order.discounts.length > 0,
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (order.shippingPrice.net.amount !== 0) {
|
|
||||||
// * In Avatax, shipping is a regular line
|
|
||||||
const shippingLine: LineItemModel = {
|
|
||||||
amount: order.shippingPrice.gross.amount,
|
|
||||||
taxIncluded: true,
|
|
||||||
itemCode: SHIPPING_ITEM_CODE,
|
|
||||||
/**
|
|
||||||
* * Different shipping methods can have different tax codes.
|
|
||||||
* https://developer.avalara.com/ecommerce-integration-guide/sales-tax-badge/designing/non-standard-items/\
|
|
||||||
*/
|
|
||||||
taxCode: config.shippingTaxCode,
|
|
||||||
quantity: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
return [...productLines, shippingLine];
|
|
||||||
}
|
|
||||||
|
|
||||||
return productLines;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class AvataxOrderCreatedPayloadTransformer {
|
export class AvataxOrderCreatedPayloadTransformer {
|
||||||
constructor(private readonly providerConfig: AvataxConfig) {}
|
transform(
|
||||||
transform = ({ order }: { order: OrderCreatedSubscriptionFragment }): CreateTransactionArgs => {
|
order: OrderCreatedSubscriptionFragment,
|
||||||
|
avataxConfig: AvataxConfig,
|
||||||
|
matches: AvataxTaxCodeMatches
|
||||||
|
): CreateTransactionArgs {
|
||||||
|
const linesTransformer = new AvataxOrderCreatedPayloadLinesTransformer();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
model: {
|
model: {
|
||||||
type: DocumentType.SalesInvoice,
|
type: DocumentType.SalesInvoice,
|
||||||
customerCode:
|
customerCode:
|
||||||
order.user?.id ??
|
order.user?.id ??
|
||||||
"" /* In Saleor Avatax plugin, the customer code is 0. In Taxes App, we set it to the user id. */,
|
"" /* In Saleor Avatax plugin, the customer code is 0. In Taxes App, we set it to the user id. */,
|
||||||
companyCode: this.providerConfig.companyCode,
|
companyCode: avataxConfig.companyCode,
|
||||||
// * commit: If true, the transaction will be committed immediately after it is created. See: https://developer.avalara.com/communications/dev-guide_rest_v2/commit-uncommit
|
// * commit: If true, the transaction will be committed immediately after it is created. See: https://developer.avalara.com/communications/dev-guide_rest_v2/commit-uncommit
|
||||||
commit: this.providerConfig.isAutocommit,
|
commit: avataxConfig.isAutocommit,
|
||||||
addresses: {
|
addresses: {
|
||||||
shipFrom: avataxAddressFactory.fromChannelAddress(this.providerConfig.address),
|
shipFrom: avataxAddressFactory.fromChannelAddress(avataxConfig.address),
|
||||||
// billing or shipping address?
|
// billing or shipping address?
|
||||||
shipTo: avataxAddressFactory.fromSaleorAddress(order.billingAddress!),
|
shipTo: avataxAddressFactory.fromSaleorAddress(order.billingAddress!),
|
||||||
},
|
},
|
||||||
currencyCode: order.total.currency,
|
currencyCode: order.total.currency,
|
||||||
email: order.user?.email ?? "",
|
email: order.user?.email ?? "",
|
||||||
lines: mapLines(order, this.providerConfig),
|
lines: linesTransformer.transform(order, avataxConfig, matches),
|
||||||
date: new Date(order.created),
|
date: new Date(order.created),
|
||||||
discount: discountUtils.sumDiscounts(
|
discount: discountUtils.sumDiscounts(
|
||||||
order.discounts.map((discount) => discount.amount.amount)
|
order.discounts.map((discount) => discount.amount.amount)
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { AuthData } from "@saleor/app-sdk/APL";
|
||||||
|
import { OrderCreatedSubscriptionFragment } from "../../../../generated/graphql";
|
||||||
|
import { CreateTransactionArgs } from "../avatax-client";
|
||||||
|
import { AvataxConfig } from "../avatax-connection-schema";
|
||||||
|
import { AvataxTaxCodeMatchesService } from "../tax-code/avatax-tax-code-matches.service";
|
||||||
|
import { AvataxOrderCreatedPayloadTransformer } from "./avatax-order-created-payload-transformer";
|
||||||
|
|
||||||
|
export class AvataxOrderCreatedPayloadService {
|
||||||
|
constructor(private authData: AuthData) {}
|
||||||
|
|
||||||
|
private getMatches() {
|
||||||
|
const taxCodeMatchesService = new AvataxTaxCodeMatchesService(this.authData);
|
||||||
|
|
||||||
|
return taxCodeMatchesService.getAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPayload(
|
||||||
|
order: OrderCreatedSubscriptionFragment,
|
||||||
|
avataxConfig: AvataxConfig
|
||||||
|
): Promise<CreateTransactionArgs> {
|
||||||
|
const matches = await this.getMatches();
|
||||||
|
const payloadTransformer = new AvataxOrderCreatedPayloadTransformer();
|
||||||
|
|
||||||
|
return payloadTransformer.transform(order, avataxConfig, matches);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { OrderLineFragment } from "../../../../generated/graphql";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { AvataxTaxCodeMatches } from "../tax-code/avatax-tax-code-match-repository";
|
||||||
|
import { AvataxOrderCreatedTaxCodeMatcher } from "./avatax-order-created-tax-code-matcher";
|
||||||
|
|
||||||
|
const mockedLine: OrderLineFragment = {
|
||||||
|
productSku: "sku",
|
||||||
|
productName: "Test product",
|
||||||
|
quantity: 1,
|
||||||
|
taxClass: {
|
||||||
|
id: "tax-class-id-2",
|
||||||
|
},
|
||||||
|
unitPrice: {
|
||||||
|
net: {
|
||||||
|
amount: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
totalPrice: {
|
||||||
|
net: {
|
||||||
|
amount: 10,
|
||||||
|
},
|
||||||
|
tax: {
|
||||||
|
amount: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const matches: AvataxTaxCodeMatches = [
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
saleorTaxClassId: "tax-class-id",
|
||||||
|
avataxTaxCode: "P0000000",
|
||||||
|
},
|
||||||
|
id: "id-1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
saleorTaxClassId: "tax-class-id-3",
|
||||||
|
avataxTaxCode: "P0000001",
|
||||||
|
},
|
||||||
|
id: "id-2",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
describe("AvataxOrderCreatedTaxCodeMatcher", () => {
|
||||||
|
it("should return empty string if tax class is not found", () => {
|
||||||
|
const matcher = new AvataxOrderCreatedTaxCodeMatcher();
|
||||||
|
|
||||||
|
expect(matcher.match(mockedLine, matches)).toEqual("");
|
||||||
|
});
|
||||||
|
it("should return tax code if tax class is found", () => {
|
||||||
|
const line = structuredClone({ ...mockedLine, taxClass: { id: "tax-class-id" } });
|
||||||
|
const matcher = new AvataxOrderCreatedTaxCodeMatcher();
|
||||||
|
|
||||||
|
expect(matcher.match(line, matches)).toEqual("P0000000");
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { OrderLineFragment } from "../../../../generated/graphql";
|
||||||
|
import { AvataxTaxCodeMatches } from "../tax-code/avatax-tax-code-match-repository";
|
||||||
|
|
||||||
|
export class AvataxOrderCreatedTaxCodeMatcher {
|
||||||
|
private mapTaxClassWithTaxMatch(taxClassId: string, matches: AvataxTaxCodeMatches) {
|
||||||
|
return matches.find((m) => m.data.saleorTaxClassId === taxClassId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTaxClassId(line: OrderLineFragment): string | undefined {
|
||||||
|
return line.taxClass?.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
match(line: OrderLineFragment, matches: AvataxTaxCodeMatches) {
|
||||||
|
const taxClassId = this.getTaxClassId(line);
|
||||||
|
|
||||||
|
// We can fall back to empty string if we don't have a tax code match
|
||||||
|
return taxClassId
|
||||||
|
? this.mapTaxClassWithTaxMatch(taxClassId, matches)?.data.avataxTaxCode ?? ""
|
||||||
|
: "";
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,7 +4,7 @@ import {
|
||||||
TaxJarCalculateTaxesPayload,
|
TaxJarCalculateTaxesPayload,
|
||||||
TaxJarCalculateTaxesTarget,
|
TaxJarCalculateTaxesTarget,
|
||||||
} from "./taxjar-calculate-taxes-adapter";
|
} from "./taxjar-calculate-taxes-adapter";
|
||||||
import { TaxJarTaxCodeMatcher } from "../tax-code/taxjar-tax-code-matcher";
|
import { TaxJarCalculateTaxesTaxCodeMatcher } from "./taxjar-calculate-taxes-tax-code-matcher";
|
||||||
|
|
||||||
export class TaxJarCalculateTaxesPayloadLinesTransformer {
|
export class TaxJarCalculateTaxesPayloadLinesTransformer {
|
||||||
transform(
|
transform(
|
||||||
|
@ -21,9 +21,9 @@ export class TaxJarCalculateTaxesPayloadLinesTransformer {
|
||||||
|
|
||||||
const mappedLines: TaxJarCalculateTaxesTarget["params"]["line_items"] = lines.map(
|
const mappedLines: TaxJarCalculateTaxesTarget["params"]["line_items"] = lines.map(
|
||||||
(line, index) => {
|
(line, index) => {
|
||||||
const matcher = new TaxJarTaxCodeMatcher();
|
const matcher = new TaxJarCalculateTaxesTaxCodeMatcher();
|
||||||
const discountAmount = distributedDiscounts[index];
|
|
||||||
const taxCode = matcher.match(line, matches);
|
const taxCode = matcher.match(line, matches);
|
||||||
|
const discountAmount = distributedDiscounts[index];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: line.sourceLine.id,
|
id: line.sourceLine.id,
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { TaxBaseLineFragment } from "../../../../generated/graphql";
|
import { TaxBaseLineFragment } from "../../../../generated/graphql";
|
||||||
import { TaxJarTaxCodeMatches } from "./taxjar-tax-code-match-repository";
|
import { TaxJarTaxCodeMatches } from "../tax-code/taxjar-tax-code-match-repository";
|
||||||
import { TaxJarTaxCodeMatcher } from "./taxjar-tax-code-matcher";
|
import { TaxJarCalculateTaxesTaxCodeMatcher } from "./taxjar-calculate-taxes-tax-code-matcher";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
const matcher = new TaxJarTaxCodeMatcher();
|
const matcher = new TaxJarCalculateTaxesTaxCodeMatcher();
|
||||||
|
|
||||||
describe("TaxJarTaxCodeMatcher", () => {
|
describe("TaxJarCalculateTaxesTaxCodeMatcher", () => {
|
||||||
it("returns empty string when tax class is not found", () => {
|
it("returns empty string when tax class is not found", () => {
|
||||||
const line: TaxBaseLineFragment = {
|
const line: TaxBaseLineFragment = {
|
||||||
quantity: 1,
|
quantity: 1,
|
|
@ -1,7 +1,7 @@
|
||||||
import { TaxBaseLineFragment } from "../../../../generated/graphql";
|
import { TaxBaseLineFragment } from "../../../../generated/graphql";
|
||||||
import { TaxJarTaxCodeMatches } from "./taxjar-tax-code-match-repository";
|
import { TaxJarTaxCodeMatches } from "../tax-code/taxjar-tax-code-match-repository";
|
||||||
|
|
||||||
export class TaxJarTaxCodeMatcher {
|
export class TaxJarCalculateTaxesTaxCodeMatcher {
|
||||||
private mapTaxClassWithTaxMatch(taxClassId: string, matches: TaxJarTaxCodeMatches) {
|
private mapTaxClassWithTaxMatch(taxClassId: string, matches: TaxJarTaxCodeMatches) {
|
||||||
return matches.find((m) => m.data.saleorTaxClassId === taxClassId);
|
return matches.find((m) => m.data.saleorTaxClassId === taxClassId);
|
||||||
}
|
}
|
|
@ -1,10 +1,11 @@
|
||||||
|
import { AuthData } from "@saleor/app-sdk/APL";
|
||||||
import { OrderCreatedSubscriptionFragment } from "../../../../generated/graphql";
|
import { OrderCreatedSubscriptionFragment } from "../../../../generated/graphql";
|
||||||
import { Logger, createLogger } from "../../../lib/logger";
|
import { Logger, createLogger } from "../../../lib/logger";
|
||||||
import { CreateOrderResponse } from "../../taxes/tax-provider-webhook";
|
import { CreateOrderResponse } from "../../taxes/tax-provider-webhook";
|
||||||
import { WebhookAdapter } from "../../taxes/tax-webhook-adapter";
|
import { WebhookAdapter } from "../../taxes/tax-webhook-adapter";
|
||||||
import { CreateOrderArgs, TaxJarClient } from "../taxjar-client";
|
import { CreateOrderArgs, TaxJarClient } from "../taxjar-client";
|
||||||
import { TaxJarConfig } from "../taxjar-connection-schema";
|
import { TaxJarConfig } from "../taxjar-connection-schema";
|
||||||
import { TaxJarOrderCreatedPayloadTransformer } from "./taxjar-order-created-payload-transformer";
|
import { TaxJarOrderCreatedPayloadService } from "./taxjar-order-created-payload.service";
|
||||||
import { TaxJarOrderCreatedResponseTransformer } from "./taxjar-order-created-response-transformer";
|
import { TaxJarOrderCreatedResponseTransformer } from "./taxjar-order-created-response-transformer";
|
||||||
|
|
||||||
export type TaxJarOrderCreatedPayload = {
|
export type TaxJarOrderCreatedPayload = {
|
||||||
|
@ -17,14 +18,14 @@ export class TaxJarOrderCreatedAdapter
|
||||||
implements WebhookAdapter<TaxJarOrderCreatedPayload, TaxJarOrderCreatedResponse>
|
implements WebhookAdapter<TaxJarOrderCreatedPayload, TaxJarOrderCreatedResponse>
|
||||||
{
|
{
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
constructor(private readonly config: TaxJarConfig) {
|
constructor(private readonly config: TaxJarConfig, private authData: AuthData) {
|
||||||
this.logger = createLogger({ name: "TaxJarOrderCreatedAdapter" });
|
this.logger = createLogger({ name: "TaxJarOrderCreatedAdapter" });
|
||||||
}
|
}
|
||||||
|
|
||||||
async send(payload: TaxJarOrderCreatedPayload): Promise<TaxJarOrderCreatedResponse> {
|
async send(payload: TaxJarOrderCreatedPayload): Promise<TaxJarOrderCreatedResponse> {
|
||||||
this.logger.debug("Transforming the Saleor payload for creating order with TaxJar...");
|
this.logger.debug("Transforming the Saleor payload for creating order with TaxJar...");
|
||||||
const payloadTransformer = new TaxJarOrderCreatedPayloadTransformer(this.config);
|
const payloadService = new TaxJarOrderCreatedPayloadService(this.authData);
|
||||||
const target = payloadTransformer.transform(payload);
|
const target = await payloadService.getPayload(payload.order, this.config);
|
||||||
|
|
||||||
this.logger.debug("Calling TaxJar fetchTaxForOrder with transformed payload...");
|
this.logger.debug("Calling TaxJar fetchTaxForOrder with transformed payload...");
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
import { OrderCreatedSubscriptionFragment, OrderLineFragment } from "../../../../generated/graphql";
|
||||||
|
import { TaxJarTaxCodeMatches } from "../tax-code/taxjar-tax-code-match-repository";
|
||||||
|
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { TaxJarOrderCreatedPayloadLinesTransformer } from "./taxjar-order-created-payload-lines-transformer";
|
||||||
|
|
||||||
|
const transformer = new TaxJarOrderCreatedPayloadLinesTransformer();
|
||||||
|
|
||||||
|
const mockedLines: OrderCreatedSubscriptionFragment["lines"] = [
|
||||||
|
{
|
||||||
|
productSku: "sku",
|
||||||
|
productName: "Test product",
|
||||||
|
quantity: 1,
|
||||||
|
taxClass: {
|
||||||
|
id: "tax-class-id-2",
|
||||||
|
},
|
||||||
|
unitPrice: {
|
||||||
|
net: {
|
||||||
|
amount: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
totalPrice: {
|
||||||
|
net: {
|
||||||
|
amount: 10,
|
||||||
|
},
|
||||||
|
tax: {
|
||||||
|
amount: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
productSku: "sku-2",
|
||||||
|
productName: "Test product 2",
|
||||||
|
quantity: 2,
|
||||||
|
taxClass: {
|
||||||
|
id: "tax-class-id-3",
|
||||||
|
},
|
||||||
|
unitPrice: {
|
||||||
|
net: {
|
||||||
|
amount: 15,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
totalPrice: {
|
||||||
|
net: {
|
||||||
|
amount: 30,
|
||||||
|
},
|
||||||
|
tax: {
|
||||||
|
amount: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const matches: TaxJarTaxCodeMatches = [
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
saleorTaxClassId: "tax-class-id",
|
||||||
|
taxJarTaxCode: "P0000000",
|
||||||
|
},
|
||||||
|
id: "id-1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
saleorTaxClassId: "tax-class-id-3",
|
||||||
|
taxJarTaxCode: "P0000001",
|
||||||
|
},
|
||||||
|
id: "id-2",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
describe("TaxJarOrderCreatedPayloadLinesTransformer", () => {
|
||||||
|
it("should map payload lines correctly", () => {
|
||||||
|
expect(transformer.transform(mockedLines, matches)).toEqual([
|
||||||
|
{
|
||||||
|
quantity: 1,
|
||||||
|
unit_price: 10,
|
||||||
|
product_identifier: "sku",
|
||||||
|
product_tax_code: "",
|
||||||
|
sales_tax: 1,
|
||||||
|
description: "Test product",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
quantity: 2,
|
||||||
|
unit_price: 15,
|
||||||
|
product_identifier: "sku-2",
|
||||||
|
product_tax_code: "P0000001",
|
||||||
|
sales_tax: 3,
|
||||||
|
description: "Test product 2",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { LineItem } from "taxjar/dist/util/types";
|
||||||
|
import { OrderCreatedSubscriptionFragment } from "../../../../generated/graphql";
|
||||||
|
import { TaxJarTaxCodeMatches } from "../tax-code/taxjar-tax-code-match-repository";
|
||||||
|
import { TaxJarOrderCreatedTaxCodeMatcher } from "./taxjar-order-created-tax-code-matcher";
|
||||||
|
|
||||||
|
export class TaxJarOrderCreatedPayloadLinesTransformer {
|
||||||
|
transform(
|
||||||
|
lines: OrderCreatedSubscriptionFragment["lines"],
|
||||||
|
matches: TaxJarTaxCodeMatches
|
||||||
|
): LineItem[] {
|
||||||
|
return lines.map((line) => {
|
||||||
|
const matcher = new TaxJarOrderCreatedTaxCodeMatcher();
|
||||||
|
const taxCode = matcher.match(line, matches);
|
||||||
|
|
||||||
|
return {
|
||||||
|
quantity: line.quantity,
|
||||||
|
unit_price: line.unitPrice.net.amount,
|
||||||
|
product_identifier: line.productSku ?? "",
|
||||||
|
product_tax_code: taxCode,
|
||||||
|
sales_tax: line.totalPrice.tax.amount,
|
||||||
|
description: line.productName,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,12 +9,10 @@ const mockGenerator = new TaxJarOrderCreatedMockGenerator();
|
||||||
|
|
||||||
describe("TaxJarOrderCreatedPayloadTransformer", () => {
|
describe("TaxJarOrderCreatedPayloadTransformer", () => {
|
||||||
it("returns the correct order amount", () => {
|
it("returns the correct order amount", () => {
|
||||||
const payloadMock = {
|
const orderMock = mockGenerator.generateOrder();
|
||||||
order: mockGenerator.generateOrder(),
|
|
||||||
};
|
|
||||||
const providerConfig = mockGenerator.generateProviderConfig();
|
const providerConfig = mockGenerator.generateProviderConfig();
|
||||||
const transformer = new TaxJarOrderCreatedPayloadTransformer(providerConfig);
|
const transformer = new TaxJarOrderCreatedPayloadTransformer();
|
||||||
const transformedPayload = transformer.transform(payloadMock);
|
const transformedPayload = transformer.transform(orderMock, providerConfig, []);
|
||||||
|
|
||||||
expect(transformedPayload.params.amount).toBe(239.17);
|
expect(transformedPayload.params.amount).toBe(239.17);
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,11 +2,10 @@ import { LineItem } from "taxjar/dist/util/types";
|
||||||
import { OrderCreatedSubscriptionFragment } from "../../../../generated/graphql";
|
import { OrderCreatedSubscriptionFragment } from "../../../../generated/graphql";
|
||||||
import { numbers } from "../../taxes/numbers";
|
import { numbers } from "../../taxes/numbers";
|
||||||
import { taxProviderUtils } from "../../taxes/tax-provider-utils";
|
import { taxProviderUtils } from "../../taxes/tax-provider-utils";
|
||||||
|
import { TaxJarTaxCodeMatches } from "../tax-code/taxjar-tax-code-match-repository";
|
||||||
import { TaxJarConfig } from "../taxjar-connection-schema";
|
import { TaxJarConfig } from "../taxjar-connection-schema";
|
||||||
import {
|
import { TaxJarOrderCreatedTarget } from "./taxjar-order-created-adapter";
|
||||||
TaxJarOrderCreatedPayload,
|
import { TaxJarOrderCreatedPayloadLinesTransformer } from "./taxjar-order-created-payload-lines-transformer";
|
||||||
TaxJarOrderCreatedTarget,
|
|
||||||
} from "./taxjar-order-created-adapter";
|
|
||||||
|
|
||||||
export function sumPayloadLines(lines: LineItem[]): number {
|
export function sumPayloadLines(lines: LineItem[]): number {
|
||||||
return numbers.roundFloatToTwoDecimals(
|
return numbers.roundFloatToTwoDecimals(
|
||||||
|
@ -27,21 +26,13 @@ export function sumPayloadLines(lines: LineItem[]): number {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TaxJarOrderCreatedPayloadTransformer {
|
export class TaxJarOrderCreatedPayloadTransformer {
|
||||||
constructor(private readonly config: TaxJarConfig) {}
|
transform(
|
||||||
private mapLines(lines: OrderCreatedSubscriptionFragment["lines"]): LineItem[] {
|
order: OrderCreatedSubscriptionFragment,
|
||||||
return lines.map((line) => ({
|
taxJarConfig: TaxJarConfig,
|
||||||
quantity: line.quantity,
|
matches: TaxJarTaxCodeMatches
|
||||||
unit_price: line.unitPrice.net.amount,
|
): TaxJarOrderCreatedTarget {
|
||||||
product_identifier: line.productSku ?? "",
|
const linesTransformer = new TaxJarOrderCreatedPayloadLinesTransformer();
|
||||||
// todo: add from tax code matcher
|
const lineItems = linesTransformer.transform(order.lines, matches);
|
||||||
product_tax_code: "",
|
|
||||||
sales_tax: line.totalPrice.tax.amount,
|
|
||||||
description: line.productName,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
transform({ order }: TaxJarOrderCreatedPayload): TaxJarOrderCreatedTarget {
|
|
||||||
const lineItems = this.mapLines(order.lines);
|
|
||||||
const lineSum = sumPayloadLines(lineItems);
|
const lineSum = sumPayloadLines(lineItems);
|
||||||
const shippingAmount = order.shippingPrice.gross.amount;
|
const shippingAmount = order.shippingPrice.gross.amount;
|
||||||
/**
|
/**
|
||||||
|
@ -52,11 +43,11 @@ export class TaxJarOrderCreatedPayloadTransformer {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
params: {
|
params: {
|
||||||
from_country: this.config.address.country,
|
from_country: taxJarConfig.address.country,
|
||||||
from_zip: this.config.address.zip,
|
from_zip: taxJarConfig.address.zip,
|
||||||
from_state: this.config.address.state,
|
from_state: taxJarConfig.address.state,
|
||||||
from_city: this.config.address.city,
|
from_city: taxJarConfig.address.city,
|
||||||
from_street: this.config.address.street,
|
from_street: taxJarConfig.address.street,
|
||||||
to_country: order.shippingAddress!.country.code,
|
to_country: order.shippingAddress!.country.code,
|
||||||
to_zip: order.shippingAddress!.postalCode,
|
to_zip: order.shippingAddress!.postalCode,
|
||||||
to_state: order.shippingAddress!.countryArea,
|
to_state: order.shippingAddress!.countryArea,
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { AuthData } from "@saleor/app-sdk/APL";
|
||||||
|
import { OrderCreatedSubscriptionFragment } from "../../../../generated/graphql";
|
||||||
|
import { TaxJarTaxCodeMatchesService } from "../tax-code/taxjar-tax-code-matches.service";
|
||||||
|
import { TaxJarConfig } from "../taxjar-connection-schema";
|
||||||
|
import { TaxJarOrderCreatedPayloadTransformer } from "./taxjar-order-created-payload-transformer";
|
||||||
|
import { CreateOrderArgs } from "../taxjar-client";
|
||||||
|
|
||||||
|
export class TaxJarOrderCreatedPayloadService {
|
||||||
|
constructor(private authData: AuthData) {}
|
||||||
|
|
||||||
|
private getMatches() {
|
||||||
|
const taxCodeMatchesService = new TaxJarTaxCodeMatchesService(this.authData);
|
||||||
|
|
||||||
|
return taxCodeMatchesService.getAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPayload(
|
||||||
|
order: OrderCreatedSubscriptionFragment,
|
||||||
|
taxJarConfig: TaxJarConfig
|
||||||
|
): Promise<CreateOrderArgs> {
|
||||||
|
const matches = await this.getMatches();
|
||||||
|
const payloadTransformer = new TaxJarOrderCreatedPayloadTransformer();
|
||||||
|
|
||||||
|
return payloadTransformer.transform(order, taxJarConfig, matches);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { OrderLineFragment } from "../../../../generated/graphql";
|
||||||
|
import { TaxJarTaxCodeMatches } from "../tax-code/taxjar-tax-code-match-repository";
|
||||||
|
import { TaxJarOrderCreatedTaxCodeMatcher } from "./taxjar-order-created-tax-code-matcher";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
const mockedLine: OrderLineFragment = {
|
||||||
|
productSku: "sku",
|
||||||
|
productName: "Test product",
|
||||||
|
quantity: 1,
|
||||||
|
taxClass: {
|
||||||
|
id: "tax-class-id-2",
|
||||||
|
},
|
||||||
|
unitPrice: {
|
||||||
|
net: {
|
||||||
|
amount: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
totalPrice: {
|
||||||
|
net: {
|
||||||
|
amount: 10,
|
||||||
|
},
|
||||||
|
tax: {
|
||||||
|
amount: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const matches: TaxJarTaxCodeMatches = [
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
saleorTaxClassId: "tax-class-id",
|
||||||
|
taxJarTaxCode: "P0000000",
|
||||||
|
},
|
||||||
|
id: "id-1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
saleorTaxClassId: "tax-class-id-3",
|
||||||
|
taxJarTaxCode: "P0000001",
|
||||||
|
},
|
||||||
|
id: "id-2",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
describe("TaxJarOrderCreatedTaxCodeMatcher", () => {
|
||||||
|
it("should return empty string if tax class is not found", () => {
|
||||||
|
const matcher = new TaxJarOrderCreatedTaxCodeMatcher();
|
||||||
|
|
||||||
|
expect(matcher.match(mockedLine, matches)).toEqual("");
|
||||||
|
});
|
||||||
|
it("should return tax code if tax class is found", () => {
|
||||||
|
const line = structuredClone({ ...mockedLine, taxClass: { id: "tax-class-id" } });
|
||||||
|
const matcher = new TaxJarOrderCreatedTaxCodeMatcher();
|
||||||
|
|
||||||
|
expect(matcher.match(line, matches)).toEqual("P0000000");
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { OrderLineFragment } from "../../../../generated/graphql";
|
||||||
|
import { TaxJarTaxCodeMatches } from "../tax-code/taxjar-tax-code-match-repository";
|
||||||
|
|
||||||
|
export class TaxJarOrderCreatedTaxCodeMatcher {
|
||||||
|
private mapTaxClassWithTaxMatch(taxClassId: string, matches: TaxJarTaxCodeMatches) {
|
||||||
|
return matches.find((m) => m.data.saleorTaxClassId === taxClassId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTaxClassId(line: OrderLineFragment): string | undefined {
|
||||||
|
return line.taxClass?.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
match(line: OrderLineFragment, matches: TaxJarTaxCodeMatches) {
|
||||||
|
const taxClassId = this.getTaxClassId(line);
|
||||||
|
|
||||||
|
// We can fall back to empty string if we don't have a tax code match
|
||||||
|
return taxClassId
|
||||||
|
? this.mapTaxClassWithTaxMatch(taxClassId, matches)?.data.taxJarTaxCode ?? ""
|
||||||
|
: "";
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,7 +31,7 @@ export class TaxJarWebhookService implements ProviderWebhookService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOrder(order: OrderCreatedSubscriptionFragment) {
|
async createOrder(order: OrderCreatedSubscriptionFragment) {
|
||||||
const adapter = new TaxJarOrderCreatedAdapter(this.config);
|
const adapter = new TaxJarOrderCreatedAdapter(this.config, this.authData);
|
||||||
|
|
||||||
const response = await adapter.send({ order });
|
const response = await adapter.send({ order });
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue