feat: taxes map entity type (#808)
* feat: ✨ add privateMetadata to order in subscription * feat: ✨ add avatax-entity-type-matcher * test: ✅ add tests for entity-type-matcher * refactor: ♻️ use metadata instead of privateMetadata * refactor: ♻️ replace brs * chore: 💡 remove todo comment * build: 👷 add changeset * refactor: ♻️ graphql queries with metafield instead metadata
This commit is contained in:
parent
e9531ce79f
commit
a725720920
16 changed files with 189 additions and 58 deletions
5
.changeset/chatty-mugs-accept.md
Normal file
5
.changeset/chatty-mugs-accept.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
"saleor-app-taxes": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Added the possibility to pass entityUseCode as order `avataxEntityCode` metadata field. This makes tax exempting groups of customers possible.
|
|
@ -61,11 +61,13 @@ fragment TaxBase on TaxableObject {
|
||||||
}
|
}
|
||||||
sourceObject {
|
sourceObject {
|
||||||
... on Checkout {
|
... on Checkout {
|
||||||
|
avataxEntityCode: metafield(key: "avataxEntityCode")
|
||||||
user {
|
user {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
... on Order {
|
... on Order {
|
||||||
|
avataxEntityCode: metafield(key: "avataxEntityCode")
|
||||||
user {
|
user {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,6 +64,7 @@ fragment OrderCreatedSubscription on Order {
|
||||||
amount
|
amount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
avataxEntityCode: metafield(key: "avataxEntityCode")
|
||||||
}
|
}
|
||||||
fragment OrderCreatedEventSubscription on Event {
|
fragment OrderCreatedEventSubscription on Event {
|
||||||
__typename
|
__typename
|
||||||
|
|
|
@ -6,6 +6,7 @@ export const defaultOrder: OrderCreatedSubscriptionFragment = {
|
||||||
id: "VXNlcjoyMDg0NTEwNDEw",
|
id: "VXNlcjoyMDg0NTEwNDEw",
|
||||||
email: "happy.customer@saleor.io",
|
email: "happy.customer@saleor.io",
|
||||||
},
|
},
|
||||||
|
avataxEntityCode: null,
|
||||||
created: "2023-05-25T09:18:55.203440+00:00",
|
created: "2023-05-25T09:18:55.203440+00:00",
|
||||||
status: OrderStatus.Unfulfilled,
|
status: OrderStatus.Unfulfilled,
|
||||||
channel: {
|
channel: {
|
||||||
|
|
|
@ -86,4 +86,11 @@ export class AvataxClient {
|
||||||
async ping() {
|
async ping() {
|
||||||
return this.client.ping();
|
return this.client.ping();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getEntityUseCode(useCode: string) {
|
||||||
|
return this.client.listEntityUseCodes({
|
||||||
|
// https://developer.avalara.com/avatax/filtering-in-rest/
|
||||||
|
filter: `code eq ${useCode}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { AvataxClient } from "./avatax-client";
|
||||||
|
import { AvataxEntityTypeMatcher } from "./avatax-entity-type-matcher";
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
const mockGetEntityUseCode = vi.fn();
|
||||||
|
|
||||||
|
describe("AvataxEntityTypeMatcher", () => {
|
||||||
|
it("returns empty string when no entity code", async () => {
|
||||||
|
const mockAvataxClient = {
|
||||||
|
getEntityUseCode: mockGetEntityUseCode.mockReturnValue(
|
||||||
|
Promise.resolve({ value: [{ code: "entityCode" }] })
|
||||||
|
),
|
||||||
|
} as any as AvataxClient;
|
||||||
|
|
||||||
|
const matcher = new AvataxEntityTypeMatcher({ client: mockAvataxClient });
|
||||||
|
const result = await matcher.match(null);
|
||||||
|
|
||||||
|
expect(result).toBe("");
|
||||||
|
});
|
||||||
|
it("returns empty string when entity code is present in metadata but not in avatax", async () => {
|
||||||
|
const mockAvataxClient = {
|
||||||
|
getEntityUseCode: mockGetEntityUseCode.mockReturnValue(Promise.resolve({})),
|
||||||
|
} as any as AvataxClient;
|
||||||
|
|
||||||
|
const matcher = new AvataxEntityTypeMatcher({ client: mockAvataxClient });
|
||||||
|
|
||||||
|
const result = await matcher.match("entityCode");
|
||||||
|
|
||||||
|
expect(result).toBe("");
|
||||||
|
});
|
||||||
|
it("returns entity code when entity code is present in metadata and in avatax", async () => {
|
||||||
|
const mockAvataxClient = {
|
||||||
|
getEntityUseCode: mockGetEntityUseCode.mockReturnValue(
|
||||||
|
Promise.resolve({ value: [{ code: "entityCode" }] })
|
||||||
|
),
|
||||||
|
} as any as AvataxClient;
|
||||||
|
|
||||||
|
const matcher = new AvataxEntityTypeMatcher({ client: mockAvataxClient });
|
||||||
|
|
||||||
|
const result = await matcher.match("entityCode");
|
||||||
|
|
||||||
|
expect(result).toBe("entityCode");
|
||||||
|
});
|
||||||
|
});
|
45
apps/taxes/src/modules/avatax/avatax-entity-type-matcher.ts
Normal file
45
apps/taxes/src/modules/avatax/avatax-entity-type-matcher.ts
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import { Logger, createLogger } from "../../lib/logger";
|
||||||
|
import { AvataxClient } from "./avatax-client";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Arbitrary key-value pair that is used to store the entity code in the metadata.
|
||||||
|
* see: https://docs.saleor.io/docs/3.x/developer/app-store/apps/taxes/avatax#mapping-the-entity-type
|
||||||
|
*/
|
||||||
|
const AVATAX_ENTITY_CODE = "avataxEntityCode";
|
||||||
|
|
||||||
|
export class AvataxEntityTypeMatcher {
|
||||||
|
private client: AvataxClient;
|
||||||
|
private logger: Logger;
|
||||||
|
|
||||||
|
constructor({ client }: { client: AvataxClient }) {
|
||||||
|
this.client = client;
|
||||||
|
this.logger = createLogger({
|
||||||
|
name: "AvataxEntityTypeMatcher",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private returnFallback() {
|
||||||
|
// Empty string will be treated as non existing entity code.
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private async validateEntityCode(entityCode: string) {
|
||||||
|
const result = await this.client.getEntityUseCode(entityCode);
|
||||||
|
|
||||||
|
// If verified, return the entity code. If not, return empty string.
|
||||||
|
return result.value?.[0].code || this.returnFallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
async match(entityCode: string | null | undefined) {
|
||||||
|
if (!entityCode) {
|
||||||
|
return this.returnFallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return this.validateEntityCode(entityCode);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.debug({ error }, "Failed to verify entity code");
|
||||||
|
return this.returnFallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -106,6 +106,7 @@ const defaultTaxBase: TaxBase = {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
sourceObject: {
|
sourceObject: {
|
||||||
|
avataxEntityCode: null,
|
||||||
user: {
|
user: {
|
||||||
id: "VXNlcjoyMDg0NTEwNDEw",
|
id: "VXNlcjoyMDg0NTEwNDEw",
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,11 +7,11 @@ const mockGenerator = new AvataxCalculateTaxesMockGenerator();
|
||||||
const avataxConfigMock = mockGenerator.generateAvataxConfig();
|
const avataxConfigMock = mockGenerator.generateAvataxConfig();
|
||||||
|
|
||||||
describe("AvataxCalculateTaxesPayloadTransformer", () => {
|
describe("AvataxCalculateTaxesPayloadTransformer", () => {
|
||||||
it("returns document type of SalesInvoice", () => {
|
it("returns document type of SalesInvoice", async () => {
|
||||||
const taxBaseMock = mockGenerator.generateTaxBase();
|
const taxBaseMock = mockGenerator.generateTaxBase();
|
||||||
const matchesMock = mockGenerator.generateTaxCodeMatches();
|
const matchesMock = mockGenerator.generateTaxCodeMatches();
|
||||||
|
|
||||||
const payload = new AvataxCalculateTaxesPayloadTransformer().transform(
|
const payload = await new AvataxCalculateTaxesPayloadTransformer().transform(
|
||||||
taxBaseMock,
|
taxBaseMock,
|
||||||
avataxConfigMock,
|
avataxConfigMock,
|
||||||
matchesMock
|
matchesMock
|
||||||
|
@ -19,11 +19,11 @@ describe("AvataxCalculateTaxesPayloadTransformer", () => {
|
||||||
|
|
||||||
expect(payload.model.type).toBe(DocumentType.SalesOrder);
|
expect(payload.model.type).toBe(DocumentType.SalesOrder);
|
||||||
});
|
});
|
||||||
it("when discounts, calculates the sum of discounts", () => {
|
it("when discounts, calculates the sum of discounts", async () => {
|
||||||
const taxBaseMock = mockGenerator.generateTaxBase({ discounts: [{ amount: { amount: 10 } }] });
|
const taxBaseMock = mockGenerator.generateTaxBase({ discounts: [{ amount: { amount: 10 } }] });
|
||||||
const matchesMock = mockGenerator.generateTaxCodeMatches();
|
const matchesMock = mockGenerator.generateTaxCodeMatches();
|
||||||
|
|
||||||
const payload = new AvataxCalculateTaxesPayloadTransformer().transform(
|
const payload = await new AvataxCalculateTaxesPayloadTransformer().transform(
|
||||||
taxBaseMock,
|
taxBaseMock,
|
||||||
avataxConfigMock,
|
avataxConfigMock,
|
||||||
matchesMock
|
matchesMock
|
||||||
|
@ -31,11 +31,13 @@ describe("AvataxCalculateTaxesPayloadTransformer", () => {
|
||||||
|
|
||||||
expect(payload.model.discount).toEqual(10);
|
expect(payload.model.discount).toEqual(10);
|
||||||
});
|
});
|
||||||
it("when no discounts, the sum of discount is 0", () => {
|
it("when no discounts, the sum of discount is 0", async () => {
|
||||||
|
const mockGenerator = new AvataxCalculateTaxesMockGenerator();
|
||||||
|
const avataxConfigMock = mockGenerator.generateAvataxConfig();
|
||||||
const taxBaseMock = mockGenerator.generateTaxBase();
|
const taxBaseMock = mockGenerator.generateTaxBase();
|
||||||
const matchesMock = mockGenerator.generateTaxCodeMatches();
|
const matchesMock = mockGenerator.generateTaxCodeMatches();
|
||||||
|
|
||||||
const payload = new AvataxCalculateTaxesPayloadTransformer().transform(
|
const payload = await new AvataxCalculateTaxesPayloadTransformer().transform(
|
||||||
taxBaseMock,
|
taxBaseMock,
|
||||||
avataxConfigMock,
|
avataxConfigMock,
|
||||||
matchesMock
|
matchesMock
|
||||||
|
|
|
@ -2,32 +2,37 @@ import { DocumentType } from "avatax/lib/enums/DocumentType";
|
||||||
import { TaxBaseFragment } from "../../../../generated/graphql";
|
import { TaxBaseFragment } from "../../../../generated/graphql";
|
||||||
import { discountUtils } from "../../taxes/discount-utils";
|
import { discountUtils } from "../../taxes/discount-utils";
|
||||||
import { avataxAddressFactory } from "../address-factory";
|
import { avataxAddressFactory } from "../address-factory";
|
||||||
import { CreateTransactionArgs } from "../avatax-client";
|
import { AvataxClient, CreateTransactionArgs } from "../avatax-client";
|
||||||
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 { AvataxCalculateTaxesPayloadLinesTransformer } from "./avatax-calculate-taxes-payload-lines-transformer";
|
import { AvataxCalculateTaxesPayloadLinesTransformer } from "./avatax-calculate-taxes-payload-lines-transformer";
|
||||||
|
import { AvataxEntityTypeMatcher } from "../avatax-entity-type-matcher";
|
||||||
|
|
||||||
export class AvataxCalculateTaxesPayloadTransformer {
|
export class AvataxCalculateTaxesPayloadTransformer {
|
||||||
private matchDocumentType(config: AvataxConfig): DocumentType {
|
private matchDocumentType(config: AvataxConfig): DocumentType {
|
||||||
/*
|
/*
|
||||||
* * For calculating taxes, we always use DocumentType.SalesOrder because it doesn't cause transaction recording.
|
* * For calculating taxes, we always use DocumentType.SalesOrder because it doesn't cause transaction recording.
|
||||||
* * The full flow is described here: https://developer.avalara.com/ecommerce-integration-guide/sales-tax-badge/design-document-workflow/should-i-commit/
|
* * The full flow is described here: https://developer.avalara.com/ecommerce-integration-guide/sales-tax-badge/design-document-workflow/should-i-commit/
|
||||||
* * config.isDocumentRecordingEnabledEnabled is used to determine if the transaction should be recorded (hence if the document type should be SalesOrder).
|
* * config.isDocumentRecordingEnabled is used to determine if the transaction should be recorded (hence if the document type should be SalesOrder).
|
||||||
* * Given that we never want to record the transaction in calculate taxes, we always return DocumentType.SalesOrder.
|
* * Given that we never want to record the transaction in calculate taxes, we always return DocumentType.SalesOrder.
|
||||||
*/
|
*/
|
||||||
return DocumentType.SalesOrder;
|
return DocumentType.SalesOrder;
|
||||||
}
|
}
|
||||||
|
|
||||||
transform(
|
async transform(
|
||||||
taxBase: TaxBaseFragment,
|
taxBase: TaxBaseFragment,
|
||||||
avataxConfig: AvataxConfig,
|
avataxConfig: AvataxConfig,
|
||||||
matches: AvataxTaxCodeMatches
|
matches: AvataxTaxCodeMatches
|
||||||
): CreateTransactionArgs {
|
): Promise<CreateTransactionArgs> {
|
||||||
const payloadLinesTransformer = new AvataxCalculateTaxesPayloadLinesTransformer();
|
const payloadLinesTransformer = new AvataxCalculateTaxesPayloadLinesTransformer();
|
||||||
|
const avataxClient = new AvataxClient(avataxConfig);
|
||||||
|
const entityTypeMatcher = new AvataxEntityTypeMatcher({ client: avataxClient });
|
||||||
|
const entityUseCode = await entityTypeMatcher.match(taxBase.sourceObject.avataxEntityCode);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
model: {
|
model: {
|
||||||
type: this.matchDocumentType(avataxConfig),
|
type: this.matchDocumentType(avataxConfig),
|
||||||
|
entityUseCode,
|
||||||
customerCode: taxBase.sourceObject.user?.id ?? "",
|
customerCode: taxBase.sourceObject.user?.id ?? "",
|
||||||
companyCode: avataxConfig.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
|
||||||
|
|
|
@ -22,13 +22,13 @@ const transformer = new AvataxOrderCreatedPayloadTransformer();
|
||||||
export const avataxConfigMock = mockGenerator.generateAvataxConfig();
|
export const avataxConfigMock = mockGenerator.generateAvataxConfig();
|
||||||
|
|
||||||
describe("AvataxOrderCreatedPayloadTransformer", () => {
|
describe("AvataxOrderCreatedPayloadTransformer", () => {
|
||||||
it("returns document type of SalesInvoice when isDocumentRecordingEnabled is true", () => {
|
it("returns document type of SalesInvoice when isDocumentRecordingEnabled is true", async () => {
|
||||||
const payload = transformer.transform(orderMock, avataxConfigMock, []);
|
const payload = await transformer.transform(orderMock, avataxConfigMock, []);
|
||||||
|
|
||||||
expect(payload.model.type).toBe(DocumentType.SalesInvoice);
|
expect(payload.model.type).toBe(DocumentType.SalesInvoice);
|
||||||
}),
|
}),
|
||||||
it("returns document type of SalesOrder when isDocumentRecordingEnabled is false", () => {
|
it("returns document type of SalesOrder when isDocumentRecordingEnabled is false", async () => {
|
||||||
const payload = transformer.transform(
|
const payload = await transformer.transform(
|
||||||
orderMock,
|
orderMock,
|
||||||
{
|
{
|
||||||
...avataxConfigMock,
|
...avataxConfigMock,
|
||||||
|
@ -39,16 +39,17 @@ describe("AvataxOrderCreatedPayloadTransformer", () => {
|
||||||
|
|
||||||
expect(payload.model.type).toBe(DocumentType.SalesOrder);
|
expect(payload.model.type).toBe(DocumentType.SalesOrder);
|
||||||
});
|
});
|
||||||
it("returns lines with discounted: true when there are discounts", () => {
|
it("returns lines with discounted: true when there are discounts", async () => {
|
||||||
const payload = transformer.transform(discountedOrderMock, avataxConfigMock, []);
|
const payload = await 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);
|
||||||
|
|
||||||
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", async () => {
|
||||||
const payload = transformer.transform(orderMock, avataxConfigMock, []);
|
const transformer = new AvataxOrderCreatedPayloadTransformer();
|
||||||
|
const payload = await transformer.transform(orderMock, avataxConfigMock, []);
|
||||||
|
|
||||||
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);
|
||||||
|
|
|
@ -2,10 +2,11 @@ import { DocumentType } from "avatax/lib/enums/DocumentType";
|
||||||
import { OrderCreatedSubscriptionFragment } from "../../../../generated/graphql";
|
import { OrderCreatedSubscriptionFragment } from "../../../../generated/graphql";
|
||||||
import { discountUtils } from "../../taxes/discount-utils";
|
import { discountUtils } from "../../taxes/discount-utils";
|
||||||
import { avataxAddressFactory } from "../address-factory";
|
import { avataxAddressFactory } from "../address-factory";
|
||||||
import { CreateTransactionArgs } from "../avatax-client";
|
import { AvataxClient, CreateTransactionArgs } from "../avatax-client";
|
||||||
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 { AvataxOrderCreatedPayloadLinesTransformer } from "./avatax-order-created-payload-lines-transformer";
|
import { AvataxOrderCreatedPayloadLinesTransformer } from "./avatax-order-created-payload-lines-transformer";
|
||||||
|
import { AvataxEntityTypeMatcher } from "../avatax-entity-type-matcher";
|
||||||
|
|
||||||
export const SHIPPING_ITEM_CODE = "Shipping";
|
export const SHIPPING_ITEM_CODE = "Shipping";
|
||||||
|
|
||||||
|
@ -18,16 +19,20 @@ export class AvataxOrderCreatedPayloadTransformer {
|
||||||
|
|
||||||
return DocumentType.SalesInvoice;
|
return DocumentType.SalesInvoice;
|
||||||
}
|
}
|
||||||
transform(
|
async transform(
|
||||||
order: OrderCreatedSubscriptionFragment,
|
order: OrderCreatedSubscriptionFragment,
|
||||||
avataxConfig: AvataxConfig,
|
avataxConfig: AvataxConfig,
|
||||||
matches: AvataxTaxCodeMatches
|
matches: AvataxTaxCodeMatches
|
||||||
): CreateTransactionArgs {
|
): Promise<CreateTransactionArgs> {
|
||||||
const linesTransformer = new AvataxOrderCreatedPayloadLinesTransformer();
|
const linesTransformer = new AvataxOrderCreatedPayloadLinesTransformer();
|
||||||
|
const avataxClient = new AvataxClient(avataxConfig);
|
||||||
|
const entityTypeMatcher = new AvataxEntityTypeMatcher({ client: avataxClient });
|
||||||
|
const entityUseCode = await entityTypeMatcher.match(order.avataxEntityCode);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
model: {
|
model: {
|
||||||
type: this.matchDocumentType(avataxConfig),
|
type: this.matchDocumentType(avataxConfig),
|
||||||
|
entityUseCode,
|
||||||
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. */,
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { Text } from "@saleor/macaw-ui/next";
|
||||||
import { Section } from "../../ui/app-section";
|
import { Section } from "../../ui/app-section";
|
||||||
import { ChannelList } from "./channel-list";
|
import { ChannelList } from "./channel-list";
|
||||||
import { AppDashboardLink } from "../../ui/app-dashboard-link";
|
import { AppDashboardLink } from "../../ui/app-dashboard-link";
|
||||||
|
@ -9,15 +10,17 @@ const Intro = () => {
|
||||||
data-testid="channel-intro"
|
data-testid="channel-intro"
|
||||||
description={
|
description={
|
||||||
<>
|
<>
|
||||||
This table displays all the channels configured to use the tax app as the tax calculation
|
<Text as="p" marginBottom={4}>
|
||||||
method.
|
This table displays all the channels configured to use the tax app as the tax
|
||||||
<br />
|
calculation method.
|
||||||
<br />
|
</Text>
|
||||||
You can change the tax configuration method for each channel in the{" "}
|
<Text as="p">
|
||||||
<AppDashboardLink data-testid="configuration-taxes-text-link" href="/taxes/channels">
|
You can change the tax configuration method for each channel in the{" "}
|
||||||
Configuration → Taxes
|
<AppDashboardLink data-testid="configuration-taxes-text-link" href="/taxes/channels">
|
||||||
</AppDashboardLink>{" "}
|
Configuration → Taxes
|
||||||
view.
|
</AppDashboardLink>{" "}
|
||||||
|
view.
|
||||||
|
</Text>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { TextLink } from "@saleor/apps-ui";
|
import { TextLink } from "@saleor/apps-ui";
|
||||||
import { Section } from "../../ui/app-section";
|
import { Section } from "../../ui/app-section";
|
||||||
import { ProvidersList } from "./providers-list";
|
import { ProvidersList } from "./providers-list";
|
||||||
|
import { Text } from "@saleor/macaw-ui/next";
|
||||||
|
|
||||||
const Intro = () => {
|
const Intro = () => {
|
||||||
return (
|
return (
|
||||||
|
@ -9,18 +10,20 @@ const Intro = () => {
|
||||||
data-testid="providers-intro"
|
data-testid="providers-intro"
|
||||||
description={
|
description={
|
||||||
<>
|
<>
|
||||||
Saleor offers two ways of calculating taxes: flat or dynamic rates.
|
<Text as="p" marginBottom={4}>
|
||||||
<br />
|
Saleor offers two ways of calculating taxes: flat or dynamic rates.
|
||||||
<br />
|
</Text>
|
||||||
Taxes App leverages the dynamic rates by delegating the tax calculation to third-party
|
<Text as="p" marginBottom={4}>
|
||||||
services.
|
Taxes App leverages the dynamic rates by delegating the tax calculation to third-party
|
||||||
<br />
|
services.
|
||||||
<br />
|
</Text>
|
||||||
You can read more about how Saleor deals with taxes in{" "}
|
<Text as="p">
|
||||||
<TextLink newTab href="https://docs.saleor.io/docs/3.x/developer/taxes">
|
You can read more about how Saleor deals with taxes in{" "}
|
||||||
our documentation
|
<TextLink newTab href="https://docs.saleor.io/docs/3.x/developer/taxes">
|
||||||
</TextLink>
|
our documentation
|
||||||
.
|
</TextLink>
|
||||||
|
.
|
||||||
|
</Text>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -95,6 +95,7 @@ const taxIncludedTaxBase: TaxBase = {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
sourceObject: {
|
sourceObject: {
|
||||||
|
avataxEntityCode: null,
|
||||||
user: {
|
user: {
|
||||||
id: "VXNlcjoyMDg0NTEwNDEw",
|
id: "VXNlcjoyMDg0NTEwNDEw",
|
||||||
},
|
},
|
||||||
|
@ -148,6 +149,7 @@ const taxExcludedTaxBase: TaxBase = {
|
||||||
sourceLine: {
|
sourceLine: {
|
||||||
__typename: "OrderLine",
|
__typename: "OrderLine",
|
||||||
id: "T3JkZXJMaW5lOjUxZDc2ZDY1LTFhYTgtNGEzMi1hNWJhLTJkZDMzNjVhZDhlZQ==",
|
id: "T3JkZXJMaW5lOjUxZDc2ZDY1LTFhYTgtNGEzMi1hNWJhLTJkZDMzNjVhZDhlZQ==",
|
||||||
|
|
||||||
orderProductVariant: {
|
orderProductVariant: {
|
||||||
id: "UHJvZHVjdFZhcmlhbnQ6MzQ5",
|
id: "UHJvZHVjdFZhcmlhbnQ6MzQ5",
|
||||||
product: {
|
product: {
|
||||||
|
@ -170,6 +172,7 @@ const taxExcludedTaxBase: TaxBase = {
|
||||||
sourceLine: {
|
sourceLine: {
|
||||||
__typename: "OrderLine",
|
__typename: "OrderLine",
|
||||||
id: "T3JkZXJMaW5lOjlhMGJjZDhmLWFiMGQtNDJhOC04NTBhLTEyYjQ2YjJiNGIyZg==",
|
id: "T3JkZXJMaW5lOjlhMGJjZDhmLWFiMGQtNDJhOC04NTBhLTEyYjQ2YjJiNGIyZg==",
|
||||||
|
|
||||||
orderProductVariant: {
|
orderProductVariant: {
|
||||||
id: "UHJvZHVjdFZhcmlhbnQ6MzQw",
|
id: "UHJvZHVjdFZhcmlhbnQ6MzQw",
|
||||||
product: {
|
product: {
|
||||||
|
@ -190,6 +193,7 @@ const taxExcludedTaxBase: TaxBase = {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
sourceObject: {
|
sourceObject: {
|
||||||
|
avataxEntityCode: null,
|
||||||
user: {
|
user: {
|
||||||
id: "VXNlcjoyMDg0NTEwNDEw",
|
id: "VXNlcjoyMDg0NTEwNDEw",
|
||||||
},
|
},
|
||||||
|
|
|
@ -8,11 +8,13 @@ export const TaxJarInstructions = () => {
|
||||||
title={"TaxJar Configuration"}
|
title={"TaxJar Configuration"}
|
||||||
description={
|
description={
|
||||||
<>
|
<>
|
||||||
The form consists of two sections: <i>Credentials</i> and <i>Address</i>.
|
<Text as="p" marginBottom={4}>
|
||||||
<br />
|
The form consists of two sections: <i>Credentials</i> and <i>Address</i>.
|
||||||
<br />
|
</Text>
|
||||||
<i>Credentials</i> will fail if:
|
<Text as="p">
|
||||||
<Box as="ol" margin={0}>
|
<i>Credentials</i> will fail if:
|
||||||
|
</Text>
|
||||||
|
<Box as="ol" margin={0} marginBottom={4}>
|
||||||
<li>
|
<li>
|
||||||
<Text>- The API Key is incorrect.</Text>
|
<Text>- The API Key is incorrect.</Text>
|
||||||
</li>
|
</li>
|
||||||
|
@ -20,10 +22,10 @@ export const TaxJarInstructions = () => {
|
||||||
<Text>- The API Key does not match "sandbox mode" setting.</Text>
|
<Text>- The API Key does not match "sandbox mode" setting.</Text>
|
||||||
</li>
|
</li>
|
||||||
</Box>
|
</Box>
|
||||||
<br />
|
<Text as="p" marginBottom={4}>
|
||||||
<br />
|
<i>Address</i> will fail if:
|
||||||
<i>Address</i> will fail if:
|
</Text>
|
||||||
<Box as="ol" margin={0}>
|
<Box as="ol" margin={0} marginBottom={4}>
|
||||||
<li>
|
<li>
|
||||||
<Text>
|
<Text>
|
||||||
- The address does not match{" "}
|
- The address does not match{" "}
|
||||||
|
@ -34,13 +36,13 @@ export const TaxJarInstructions = () => {
|
||||||
</Text>
|
</Text>
|
||||||
</li>
|
</li>
|
||||||
</Box>
|
</Box>
|
||||||
<br />
|
<Text as="p" marginBottom={4}>
|
||||||
<br />
|
If the configuration fails, please visit the{" "}
|
||||||
If the configuration fails, please visit the{" "}
|
<TextLink href="https://developers.taxjar.com/api/reference/" newTab>
|
||||||
<TextLink href="https://developers.taxjar.com/api/reference/" newTab>
|
TaxJar documentation
|
||||||
TaxJar documentation
|
</TextLink>
|
||||||
</TextLink>
|
.
|
||||||
.
|
</Text>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
Loading…
Reference in a new issue