fix: tax code matcher QA (#680)

* feat:  bring back validateAddress in TaxJar

* feat: 💄 decrease marginLeft of AppToggle label

* refactor: 🚚 cancelButton -> leftButton

* feat: 🧱 add data-testid to all buttons and forms

* refactor: ♻️ refactor app-section to accept Box props

* feat: 🧱 add rest of data-testid

* feat:  verify connections before displaying matcher pages

* feat:  always display matcher-section

* refactor: ♻️ improve fetching tax codes by adding retry and redirect on error

* refactor: 🚚 active-connection -> get-active-connection-service && improve logs

* chore: 🔊 remove objects from logs

* docs: 📝 add TESTING.md with edge-cases
This commit is contained in:
Adrian Pilarczyk 2023-06-28 15:03:24 +02:00 committed by GitHub
parent 6250095a4e
commit d42c79f366
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 379 additions and 160 deletions

37
apps/taxes/TESTING.md Normal file
View file

@ -0,0 +1,37 @@
# Testing
The difficulty in testing the Taxes App lies in learning about the tax providers themselves. This document will attempt to outline the various edge-cases you can encounter while interacting with the Taxes App. It will be updated as we learn more about the various tax providers.
## TaxJar
### UI
#### 1. Throws "invalid credentials" error while creating a provider
If you are using the sandbox token, make sure to check the "Sandbox mode" checkbox in the Taxes App UI.
### Calculating taxes
#### 1. Transaction doesn't appear in TaxJar
[It can take up to an hour for TaxJar to process the transaction](https://support.taxjar.com/article/643-why-are-my-transactions-not-appearing-in-taxjar). Please check again after some time.
If that's not the case, make sure you are using the live token, as the sandbox token does not send transactions to TaxJar.
#### 2. Taxes App returns taxes = 0
Here are known reasons for why the Taxes App may returns taxes = 0 while calculating taxes:
1. The sales tax in this state is 0. You can check it [here](https://www.taxjar.com/resources/sales-tax/states).
2. You don't have a nexus in this state. Read up on Nexus [here](https://www.taxjar.com/sales-tax/nexus).
3. The customer address in Saleor is invalid. We validate the "ship from" address, but we can't validate the customer address. If you are using a fake database, the generated addresses may be non-existant. You can find examples of valid addresses [here](https://developers.taxjar.com/demo/).
4. TaxJar does not respond.
5. Taxes App broke.
## Avatax
### UI
#### 1. Taxes App UI throws "invalid credentials" error while creating a provider
If you are using the sandbox token, make sure to check the "Sandbox mode" checkbox in the Taxes App UI.

View file

@ -35,6 +35,19 @@ const protectedWithConfigurationService = protectedClientProcedure.use(({ next,
);
export const avataxConnectionRouter = router({
verifyConnections: protectedWithConfigurationService.query(async ({ ctx }) => {
const logger = createLogger({
name: "avataxConnectionRouter.verifyConnections",
});
logger.debug("Route verifyConnections called");
await ctx.connectionService.verifyConnections();
logger.info("Avatax connections were successfully verified");
return { ok: true };
}),
getById: protectedWithConfigurationService.input(getInputSchema).query(async ({ ctx, input }) => {
const logger = createLogger({
name: "avataxConnectionRouter.get",

View file

@ -35,4 +35,12 @@ export class PublicAvataxConnectionService {
async delete(id: string) {
return this.connectionService.delete(id);
}
async verifyConnections() {
const connections = await this.connectionService.getAll();
if (connections.length === 0) {
throw new Error("No Avatax connections found");
}
}
}

View file

@ -22,7 +22,7 @@ type AvataxConfigurationFormProps = {
onSubmit: (data: AvataxConfig) => void;
defaultValues: AvataxConfig;
isLoading: boolean;
cancelButton: React.ReactNode;
leftButton: React.ReactNode;
};
export const AvataxConfigurationForm = (props: AvataxConfigurationFormProps) => {
@ -48,7 +48,7 @@ export const AvataxConfigurationForm = (props: AvataxConfigurationFormProps) =>
<ProviderLabel name="avatax" />
</Box>
<form onSubmit={handleSubmit(submitHandler)}>
<form onSubmit={handleSubmit(submitHandler)} data-testid="avatax-configuration-form">
<Input
control={control}
name="name"
@ -211,9 +211,14 @@ export const AvataxConfigurationForm = (props: AvataxConfigurationFormProps) =>
<Divider marginY={8} />
<Box display={"flex"} justifyContent={"space-between"} alignItems={"center"}>
{props.cancelButton}
{props.leftButton}
<Button disabled={props.isLoading} type="submit" variant="primary">
<Button
disabled={props.isLoading}
type="submit"
variant="primary"
data-testid="avatax-configuration-save-button"
>
{props.isLoading ? "Saving..." : "Save"}
</Button>
</Box>

View file

@ -5,10 +5,46 @@ import { Table } from "../../ui/table";
import { Select } from "../../ui/_select";
import { Box, Text } from "@saleor/macaw-ui/next";
import { AppCard } from "../../ui/app-card";
import { useRouter } from "next/router";
const useGetTaxCodes = () => {
const { data: providers } = trpcClient.providersConfiguration.getAll.useQuery();
const { notifyError } = useDashboardNotification();
const router = useRouter();
/*
* Tax Code Matcher is only available when there is at least one connection.
* The reason for it is that we need any working credentials to fetch the provider tax codes.
*/
const firstConnectionId = providers?.[0].id;
const result = trpcClient.avataxTaxCodes.getAllForId.useQuery(
{
connectionId: firstConnectionId!,
},
{
enabled: firstConnectionId !== undefined,
// Retry once, because it's possible we may get a timeout for such a big request.
retry: 1,
}
);
React.useEffect(() => {
if (result.error) {
notifyError("Error", "Unable to fetch Avatax tax codes.");
setTimeout(() => {
router.push("/configuration");
}, 1000);
}
}, [notifyError, result.error, router]);
return result;
};
const SelectTaxCode = ({ taxClassId }: { taxClassId: string }) => {
const [value, setValue] = React.useState("");
const { notifySuccess, notifyError } = useDashboardNotification();
const { data: taxCodes = [], isLoading: isCodesLoading } = useGetTaxCodes();
const { data: avataxMatches, isLoading: isMatchesLoading } =
trpcClient.avataxMatches.getAll.useQuery();
@ -32,24 +68,6 @@ const SelectTaxCode = ({ taxClassId }: { taxClassId: string }) => {
},
});
const { data: providers } = trpcClient.providersConfiguration.getAll.useQuery();
/*
* Tax Code Matcher is only available when there is at least one connection.
* The reason for it is that we need any working credentials to fetch the provider tax codes.
*/
const firstConnectionId = providers?.[0].id;
const { data: taxCodes = [], isLoading: isCodesLoading } =
trpcClient.avataxTaxCodes.getAllForId.useQuery(
{
connectionId: firstConnectionId!,
},
{
enabled: firstConnectionId !== undefined,
}
);
const changeValue = (avataxTaxCode: string) => {
setValue(avataxTaxCode);
updateMutation({

View file

@ -37,8 +37,12 @@ export const CreateAvataxConfiguration = () => {
isLoading={isCreateLoading}
onSubmit={submitHandler}
defaultValues={defaultAvataxConfig}
cancelButton={
<Button onClick={() => router.push("/configuration")} variant="tertiary">
leftButton={
<Button
onClick={() => router.push("/configuration")}
variant="tertiary"
data-testid="create-avatax-cancel-button"
>
Cancel
</Button>
}

View file

@ -96,8 +96,8 @@ export const EditAvataxConfiguration = () => {
isLoading={isPatchLoading}
onSubmit={submitHandler}
defaultValues={data.config}
cancelButton={
<Button onClick={deleteHandler} variant="error">
leftButton={
<Button onClick={deleteHandler} variant="error" data-testid="delete-avatax-button">
Delete provider
</Button>
}

View file

@ -21,7 +21,9 @@ const NoChannelConfigured = () => {
justifyContent={"center"}
>
<Text variant="body">No channels configured yet</Text>
<Button onClick={redirectToTaxes}>Configure channels</Button>
<Button data-testid="configure-channel-button" onClick={redirectToTaxes}>
Configure channels
</Button>
</Box>
);
};
@ -43,7 +45,7 @@ export const ChannelList = () => {
const isEmpty = isFetched && !isAnyChannelConfigured;
return (
<AppCard __minHeight={"320px"} height="100%">
<AppCard __minHeight={"320px"} height="100%" data-testid="channel-list">
{isFetching ? (
<Skeleton />
) : (

View file

@ -6,6 +6,7 @@ const Intro = () => {
return (
<Section.Description
title="Available channels"
data-testid="channel-intro"
description={
<>
This table displays all the channels configured to use the tax app as the tax calculation
@ -13,7 +14,10 @@ const Intro = () => {
<br />
<br />
You can change the tax configuration method for each channel in the{" "}
<AppDashboardLink href="/taxes/channels">Configuration Taxes</AppDashboardLink> view.
<AppDashboardLink data-testid="configuration-taxes-text-link" href="/taxes/channels">
Configuration Taxes
</AppDashboardLink>{" "}
view.
</>
}
/>

View file

@ -17,7 +17,9 @@ const AddProvider = () => {
justifyContent={"center"}
>
<Text variant="body">No providers configured yet</Text>
<Button onClick={() => router.push("/providers")}>Add first provider</Button>
<Button data-testid="no-providers-list-add-button" onClick={() => router.push("/providers")}>
Add first provider
</Button>
</Box>
);
};
@ -40,7 +42,7 @@ export const ProvidersList = () => {
const isNoResult = isFetched && !isProvider;
return (
<AppCard __minHeight={"320px"} height="100%">
<AppCard __minHeight={"320px"} height="100%" data-testid="providers-list">
{isFetching ? (
<Skeleton />
) : (
@ -55,7 +57,12 @@ export const ProvidersList = () => {
>
<ProvidersTable />
<Box display={"flex"} justifyContent={"flex-end"}>
<Button onClick={() => router.push("/providers")}>Add new</Button>
<Button
data-testid="providers-list-add-button"
onClick={() => router.push("/providers")}
>
Add new
</Button>
</Box>
</Box>
)}

View file

@ -6,6 +6,7 @@ const Intro = () => {
return (
<Section.Description
title="Tax providers"
data-testid="providers-intro"
description={
<>
Saleor offers two ways of calculating taxes: flat or dynamic rates.

View file

@ -3,7 +3,7 @@ import { describe, expect, it, vi } from "vitest";
import { MetadataItem } from "../../../generated/graphql";
import { ChannelsConfig } from "../channel-configuration/channel-config";
import { ProviderConnections } from "../provider-connections/provider-connections";
import { getActiveConnection } from "./active-connection";
import { getActiveConnectionService } from "./get-active-connection-service";
import { AuthData } from "@saleor/app-sdk/APL";
const mockedInvalidMetadata: MetadataItem[] = [
@ -95,22 +95,22 @@ const mockedAuthData: AuthData = {
vi.stubEnv("SECRET_KEY", mockedSecretKey);
describe("getActiveConnection", () => {
describe("getActiveConnectionService", () => {
it("throws error when channel slug is missing", () => {
expect(() => getActiveConnection("", mockedInvalidMetadata, mockedAuthData)).toThrow(
expect(() => getActiveConnectionService("", mockedInvalidMetadata, mockedAuthData)).toThrow(
"Channel slug was not found in the webhook payload"
);
});
it("throws error when there are no metadata items", () => {
expect(() => getActiveConnection("default-channel", [], mockedAuthData)).toThrow(
expect(() => getActiveConnectionService("default-channel", [], mockedAuthData)).toThrow(
"App encryptedMetadata was not found in the webhook payload"
);
});
it("throws error when no providerConnectionId was found", () => {
expect(() =>
getActiveConnection(
getActiveConnectionService(
"default-channel",
[
{
@ -129,7 +129,7 @@ describe("getActiveConnection", () => {
it("throws error when no channel was found for channelSlug", () => {
expect(() =>
getActiveConnection(
getActiveConnectionService(
"invalid-channel",
[
{
@ -147,7 +147,7 @@ describe("getActiveConnection", () => {
});
it("returns provider when data is correct", () => {
const result = getActiveConnection(
const result = getActiveConnectionService(
"default-channel",
[
{

View file

@ -14,13 +14,13 @@ import { TaxJarWebhookService } from "../taxjar/taxjar-webhook.service";
import { ProviderWebhookService } from "./tax-provider-webhook";
// todo: refactor to a factory
export class ActiveTaxProvider implements ProviderWebhookService {
class ActiveTaxProviderService implements ProviderWebhookService {
private logger: Logger;
private client: TaxJarWebhookService | AvataxWebhookService;
constructor(providerConnection: ProviderConnection, private authData: AuthData) {
this.logger = createLogger({
name: "ActiveTaxProvider",
name: "ActiveTaxProviderService",
});
const taxProviderName = providerConnection.provider;
@ -57,13 +57,13 @@ export class ActiveTaxProvider implements ProviderWebhookService {
}
}
export function getActiveConnection(
export function getActiveConnectionService(
channelSlug: string | undefined,
encryptedMetadata: MetadataItem[],
authData: AuthData
): ActiveTaxProvider {
): ActiveTaxProviderService {
const logger = createLogger({
name: "getActiveConnection",
name: "getActiveConnectionService",
});
if (!channelSlug) {
@ -100,7 +100,7 @@ export function getActiveConnection(
throw new Error(`Channel config providerConnectionId does not match any providers`);
}
const taxProvider = new ActiveTaxProvider(providerConnection, authData);
const taxProvider = new ActiveTaxProviderService(providerConnection, authData);
return taxProvider;
}

View file

@ -35,4 +35,12 @@ export class PublicTaxJarConnectionService {
async delete(id: string) {
return this.connectionService.delete(id);
}
async verifyConnections() {
const connections = await this.connectionService.getAll();
if (connections.length === 0) {
throw new Error("No TaxJar connections found");
}
}
}

View file

@ -47,12 +47,11 @@ export class TaxJarClient {
}
/**
* TaxJar validateAddress doesn't work. It's turned off for now.
* In the past, we've had some problems with TaxJar validateAddress. It looks like it works now, but we should keep an eye on it.
* @see https://github.com/taxjar/taxjar-node/issues/70
* @todo Revisit this when TaxJar fixes the issue. Alternatively, create a custom validation.
*/
async validateAddress({ params }: ValidateAddressArgs) {
// return this.client.validateAddress(params);
return this.client.validateAddress(params);
}
async getTaxCodes() {

View file

@ -35,6 +35,19 @@ const protectedWithConfigurationService = protectedClientProcedure.use(({ next,
);
export const taxjarConnectionRouter = router({
verifyConnections: protectedWithConfigurationService.query(async ({ ctx }) => {
const logger = createLogger({
name: "taxjarConnectionRouter.verifyConnections",
});
logger.debug("Route verifyConnections called");
await ctx.connectionService.verifyConnections();
logger.info("TaxJar connections were successfully verified");
return { ok: true };
}),
getById: protectedWithConfigurationService.input(getInputSchema).query(async ({ ctx, input }) => {
const logger = createLogger({
name: "taxjarConnectionRouter.get",

View file

@ -37,8 +37,12 @@ export const CreateTaxJarConfiguration = () => {
isLoading={isCreateLoading}
onSubmit={submitHandler}
defaultValues={defaultTaxJarConfig}
cancelButton={
<Button onClick={() => router.push("/configuration")} variant="tertiary">
leftButton={
<Button
onClick={() => router.push("/configuration")}
variant="tertiary"
data-testid="create-taxjar-cancel-button"
>
Cancel
</Button>
}

View file

@ -95,8 +95,8 @@ export const EditTaxJarConfiguration = () => {
isLoading={isPatchLoading}
onSubmit={submitHandler}
defaultValues={data.config}
cancelButton={
<Button onClick={deleteHandler} variant="error">
leftButton={
<Button onClick={deleteHandler} variant="error" data-testid="delete-taxjar-button">
Delete provider
</Button>
}

View file

@ -23,7 +23,7 @@ type TaxJarConfigurationFormProps = {
onSubmit: (data: TaxJarConfig) => void;
defaultValues: TaxJarConfig;
isLoading: boolean;
cancelButton: React.ReactNode;
leftButton: React.ReactNode;
};
export const TaxJarConfigurationForm = (props: TaxJarConfigurationFormProps) => {
@ -49,7 +49,7 @@ export const TaxJarConfigurationForm = (props: TaxJarConfigurationFormProps) =>
<ProviderLabel name="taxjar" />
</Box>
<form onSubmit={handleSubmit(submitHandler)}>
<form onSubmit={handleSubmit(submitHandler)} data-testid="taxjar-configuration-form">
<Input
control={control}
name="name"
@ -139,8 +139,13 @@ export const TaxJarConfigurationForm = (props: TaxJarConfigurationFormProps) =>
</Box>
<Divider marginY={8} />
<Box display={"flex"} justifyContent={"space-between"} alignItems={"center"}>
{props.cancelButton}
<Button disabled={props.isLoading} type="submit" variant="primary">
{props.leftButton}
<Button
disabled={props.isLoading}
type="submit"
variant="primary"
data-testid="taxjar-configuration-save-button"
>
{props.isLoading ? "Saving..." : "Save"}
</Button>
</Box>

View file

@ -5,6 +5,41 @@ import { Table } from "../../ui/table";
import { Select } from "../../ui/_select";
import { Box, Text } from "@saleor/macaw-ui/next";
import { AppCard } from "../../ui/app-card";
import { useRouter } from "next/router";
const useGetTaxCodes = () => {
const { data: providers } = trpcClient.providersConfiguration.getAll.useQuery();
const { notifyError } = useDashboardNotification();
const router = useRouter();
/*
* Tax Code Matcher is only available when there is at least one connection.
* The reason for it is that we need any working credentials to fetch the provider tax codes.
*/
const firstConnectionId = providers?.[0].id;
const result = trpcClient.taxJarTaxCodes.getAllForId.useQuery(
{
connectionId: firstConnectionId!,
},
{
enabled: firstConnectionId !== undefined,
// Retry once, because it's possible we may get a timeout for such a big request.
retry: 1,
}
);
React.useEffect(() => {
if (result.error) {
notifyError("Error", "Unable to fetch TaxJar tax codes.");
setTimeout(() => {
router.push("/configuration");
}, 1000);
}
}, [notifyError, result.error, router]);
return result;
};
const SelectTaxCode = ({ taxClassId }: { taxClassId: string }) => {
const [value, setValue] = React.useState("");
@ -32,23 +67,7 @@ const SelectTaxCode = ({ taxClassId }: { taxClassId: string }) => {
},
});
const { data: providers } = trpcClient.providersConfiguration.getAll.useQuery();
/*
* Tax Code Matcher is only available when there is at least one connection.
* The reason for it is that we need any working credentials to fetch the provider tax codes.
*/
const firstConnectionId = providers?.[0].id;
const { data: taxCodes = [], isLoading: isCodesLoading } =
trpcClient.taxJarTaxCodes.getAllForId.useQuery(
{
connectionId: firstConnectionId!,
},
{
enabled: firstConnectionId !== undefined,
}
);
const { data: taxCodes = [], isLoading: isCodesLoading } = useGetTaxCodes();
const changeValue = (taxJarTaxCode: string) => {
setValue(taxJarTaxCode);

View file

@ -1,11 +1,11 @@
import { Box, Text } from "@saleor/macaw-ui/next";
import { PropsWithChildren } from "react";
import { Box, PropsWithBox, Text } from "@saleor/macaw-ui/next";
import React from "react";
const MAX_WIDTH = "480px";
const Header = ({ children }: PropsWithChildren) => {
const Header = ({ children, ...props }: PropsWithBox<{ children: React.ReactNode }>) => {
return (
<Box __maxWidth={MAX_WIDTH}>
<Box __maxWidth={MAX_WIDTH} {...props}>
<Text as="p" variant="body">
{children}
</Text>
@ -16,12 +16,13 @@ const Header = ({ children }: PropsWithChildren) => {
const Description = ({
title,
description,
}: {
...props
}: PropsWithBox<{
title: React.ReactNode;
description: React.ReactNode;
}) => {
}>) => {
return (
<Box display="flex" flexDirection={"column"} gap={10} __maxWidth={MAX_WIDTH}>
<Box display="flex" flexDirection={"column"} gap={10} __maxWidth={MAX_WIDTH} {...props}>
<Text as="h3" variant="heading">
{title}
</Text>

View file

@ -22,7 +22,7 @@ export const AppToggle = <TFieldValues extends FieldValues = FieldValues>({
<Box display={"flex"} flexDirection={"column"} gap={4}>
{/* without type="button", radix toggle value change triggers form submission */}
<Toggle type="button" {...p}>
<Text marginLeft={4}>{label}</Text>
<Text marginLeft={2}>{label}</Text>
</Toggle>
{helperText}
</Box>

View file

@ -7,58 +7,75 @@ import { Table } from "./table";
import { useRouter } from "next/router";
const MatcherTable = () => {
const { data: connections = [] } = trpcClient.providersConfiguration.getAll.useQuery();
const { data: connections = [], isLoading } = trpcClient.providersConfiguration.getAll.useQuery();
const isAvatax = connections.some(({ provider }) => provider === "avatax");
const isTaxJar = connections.some(({ provider }) => provider === "taxjar");
const isConfigured = isAvatax || isTaxJar;
const router = useRouter();
return (
<AppCard __minHeight={"320px"} height="100%">
<Table.Container>
<Table.THead>
<Table.TR>
<Table.TH>Provider</Table.TH>
</Table.TR>
</Table.THead>
<Table.TBody>
{isAvatax && (
<Table.TR>
<Table.TD>
<ProviderLabel name="avatax" />
</Table.TD>
<Table.TD>
<Box display="flex" justifyContent={"flex-end"}>
<Button
onClick={() => router.push("/providers/avatax/matcher")}
variant="tertiary"
>
Configure
</Button>{" "}
</Box>{" "}
</Table.TD>
</Table.TR>
<AppCard __minHeight={"320px"} height="100%" data-testid="matcher-table">
{isLoading ? (
<Box height="100%" display={"flex"} alignItems={"center"} justifyContent={"center"}>
<Text color="textNeutralSubdued">Loading...</Text>
</Box>
) : (
<>
{isConfigured ? (
<Table.Container>
<Table.THead>
<Table.TR>
<Table.TH>Provider</Table.TH>
</Table.TR>
</Table.THead>
<Table.TBody>
{isAvatax && (
<Table.TR>
<Table.TD>
<ProviderLabel name="avatax" />
</Table.TD>
<Table.TD>
<Box display="flex" justifyContent={"flex-end"}>
<Button
data-testid="avatax-matcher-configure-button"
onClick={() => router.push("/providers/avatax/matcher")}
variant="tertiary"
>
Configure
</Button>{" "}
</Box>{" "}
</Table.TD>
</Table.TR>
)}
{isTaxJar && (
<Table.TR>
<Table.TD>
<ProviderLabel name="taxjar" />
</Table.TD>
<Table.TD>
<Box display="flex" justifyContent={"flex-end"}>
<Button
data-testid="taxjar-matcher-configure-button"
onClick={() => router.push("/providers/taxjar/matcher")}
variant="tertiary"
>
Configure
</Button>{" "}
</Box>
</Table.TD>
</Table.TR>
)}
</Table.TBody>
</Table.Container>
) : (
<Box height="100%" display={"flex"} alignItems={"center"} justifyContent={"center"}>
<Text color="textNeutralSubdued">You must configure a tax provider first</Text>
</Box>
)}
{isTaxJar && (
<Table.TR>
<Table.TD>
<ProviderLabel name="taxjar" />
</Table.TD>
<Table.TD>
<Box display="flex" justifyContent={"flex-end"}>
<Button
onClick={() => router.push("/providers/taxjar/matcher")}
variant="tertiary"
>
Configure
</Button>{" "}
</Box>
</Table.TD>
</Table.TR>
)}
</Table.TBody>
</Table.Container>
</>
)}
</AppCard>
);
};
@ -66,6 +83,7 @@ const MatcherTable = () => {
const Intro = () => {
return (
<Section.Description
data-testid="matcher-intro"
title="Tax code matcher"
description={
<>

View file

@ -30,7 +30,9 @@ export const ProvidersTable = () => {
</Table.TD>
<Table.TD onClick={() => itemClickHandler(item)}>
<Box display={"flex"} justifyContent={"flex-end"}>
<Button variant="tertiary">Edit</Button>
<Button data-testid="provider-edit-button" variant="tertiary">
Edit
</Button>
</Box>
</Table.TD>
</Table.TR>

View file

@ -6,7 +6,7 @@ import {
import { saleorApp } from "../../../../saleor-app";
import { createLogger } from "../../../lib/logger";
import { WebhookResponse } from "../../../modules/app/webhook-response";
import { getActiveConnection } from "../../../modules/taxes/active-connection";
import { getActiveConnectionService } from "../../../modules/taxes/get-active-connection-service";
export const config = {
api: {
@ -54,10 +54,14 @@ export default checkoutCalculateTaxesSyncWebhook.createHandler(async (req, res,
try {
const appMetadata = payload.recipient?.privateMetadata ?? [];
const channelSlug = payload.taxBase.channel.slug;
const taxProvider = getActiveConnection(channelSlug, appMetadata, ctx.authData);
const activeConnectionService = getActiveConnectionService(
channelSlug,
appMetadata,
ctx.authData
);
logger.info({ taxProvider }, "Will calculate taxes using the tax provider:");
const calculatedTaxes = await taxProvider.calculateTaxes(payload.taxBase);
logger.info("Found active connection service. Calculating taxes...");
const calculatedTaxes = await activeConnectionService.calculateTaxes(payload.taxBase);
logger.info({ calculatedTaxes }, "Taxes calculated");
return webhookResponse.success(ctx.buildResponse(calculatedTaxes));

View file

@ -5,7 +5,7 @@ import {
} from "../../../../generated/graphql";
import { saleorApp } from "../../../../saleor-app";
import { createLogger } from "../../../lib/logger";
import { getActiveConnection } from "../../../modules/taxes/active-connection";
import { getActiveConnectionService } from "../../../modules/taxes/get-active-connection-service";
import { WebhookResponse } from "../../../modules/app/webhook-response";
export const config = {
@ -54,10 +54,14 @@ export default orderCalculateTaxesSyncWebhook.createHandler(async (req, res, ctx
try {
const appMetadata = payload.recipient?.privateMetadata ?? [];
const channelSlug = payload.taxBase.channel.slug;
const taxProvider = getActiveConnection(channelSlug, appMetadata, ctx.authData);
const activeConnectionService = getActiveConnectionService(
channelSlug,
appMetadata,
ctx.authData
);
logger.info({ taxProvider }, "Will calculate taxes using the tax provider:");
const calculatedTaxes = await taxProvider.calculateTaxes(payload.taxBase);
logger.info("Found active connection service. Calculating taxes...");
const calculatedTaxes = await activeConnectionService.calculateTaxes(payload.taxBase);
logger.info({ calculatedTaxes }, "Taxes calculated");
return webhookResponse.success(ctx.buildResponse(calculatedTaxes));

View file

@ -9,7 +9,7 @@ import {
} from "../../../../generated/graphql";
import { saleorApp } from "../../../../saleor-app";
import { createLogger } from "../../../lib/logger";
import { getActiveConnection } from "../../../modules/taxes/active-connection";
import { getActiveConnectionService } from "../../../modules/taxes/get-active-connection-service";
import { Client } from "urql";
import { WebhookResponse } from "../../../modules/app/webhook-response";
import { PROVIDER_ORDER_ID_KEY } from "../../../modules/avatax/order-fulfilled/avatax-order-fulfilled-payload-transformer";
@ -69,14 +69,14 @@ export default orderCreatedAsyncWebhook.createHandler(async (req, res, ctx) => {
const { saleorApiUrl, token } = authData;
const webhookResponse = new WebhookResponse(res);
logger.info({ payload }, "Handler called with payload");
logger.info("Handler called with payload");
try {
const appMetadata = payload.recipient?.privateMetadata ?? [];
const channelSlug = payload.order?.channel.slug;
const taxProvider = getActiveConnection(channelSlug, appMetadata, ctx.authData);
const taxProvider = getActiveConnectionService(channelSlug, appMetadata, ctx.authData);
logger.info({ taxProvider }, "Fetched taxProvider");
logger.info("Fetched taxProvider");
// todo: figure out what fields are needed and add validation
if (!payload.order) {

View file

@ -5,7 +5,7 @@ import {
} from "../../../../generated/graphql";
import { saleorApp } from "../../../../saleor-app";
import { createLogger } from "../../../lib/logger";
import { getActiveConnection } from "../../../modules/taxes/active-connection";
import { getActiveConnectionService } from "../../../modules/taxes/get-active-connection-service";
import { WebhookResponse } from "../../../modules/app/webhook-response";
export const config = {
api: {
@ -31,22 +31,22 @@ export default orderFulfilledAsyncWebhook.createHandler(async (req, res, ctx) =>
const { payload } = ctx;
const webhookResponse = new WebhookResponse(res);
logger.info({ payload }, "Handler called with payload");
logger.info("Handler called with payload");
try {
const appMetadata = payload.recipient?.privateMetadata ?? [];
const channelSlug = payload.order?.channel.slug;
const taxProvider = getActiveConnection(channelSlug, appMetadata, ctx.authData);
const taxProvider = getActiveConnectionService(channelSlug, appMetadata, ctx.authData);
logger.info({ taxProvider }, "Fetched taxProvider");
logger.info("Fetched taxProvider");
// todo: figure out what fields are needed and add validation
if (!payload.order) {
return webhookResponse.error(new Error("Insufficient order data"));
}
const fulfilledOrder = await taxProvider.fulfillOrder(payload.order);
await taxProvider.fulfillOrder(payload.order);
logger.info({ fulfilledOrder }, "Order fulfilled");
logger.info("Order fulfilled");
return webhookResponse.success();
} catch (error) {

View file

@ -1,6 +1,5 @@
import { ChannelSection } from "../modules/channel-configuration/ui/channel-section";
import { ProvidersSection } from "../modules/provider-connections/ui/providers-section";
import { trpcClient } from "../modules/trpc/trpc-client";
import { AppColumns } from "../modules/ui/app-columns";
import { Section } from "../modules/ui/app-section";
import { MatcherSection } from "../modules/ui/matcher-section";
@ -14,14 +13,11 @@ const Header = () => {
};
const ConfigurationPage = () => {
const { data: providers = [] } = trpcClient.providersConfiguration.getAll.useQuery();
const isProviders = providers.length > 0;
return (
<AppColumns top={<Header />}>
<ProvidersSection />
<ChannelSection />
{isProviders && <MatcherSection />}
<MatcherSection />
</AppColumns>
);
};

View file

@ -1,9 +1,12 @@
import { Text } from "@saleor/macaw-ui/next";
import { Box, Text } from "@saleor/macaw-ui/next";
import { AvataxTaxCodeMatcherTable } from "../../../modules/avatax/ui/avatax-tax-code-matcher-table";
import { AppColumns } from "../../../modules/ui/app-columns";
import { AppDashboardLink } from "../../../modules/ui/app-dashboard-link";
import { Section } from "../../../modules/ui/app-section";
import { TextLink } from "@saleor/apps-ui";
import { useDashboardNotification } from "@saleor/apps-shared";
import { useRouter } from "next/router";
import { trpcClient } from "../../../modules/trpc/trpc-client";
const Header = () => {
return <Section.Header>Match Saleor tax classes to Avatax tax codes</Section.Header>;
@ -25,7 +28,10 @@ const Description = () => {
</Text>
<Text as="p" marginBottom={4}>
If you haven&apos;t created any tax classes yet, you can do it in the{" "}
<AppDashboardLink href="/taxes/tax-classes">
<AppDashboardLink
data-testid="avatax-matcher-tax-classes-text-link"
href="/taxes/tax-classes"
>
Configuration Taxes Tax classes
</AppDashboardLink>{" "}
view.
@ -44,6 +50,24 @@ const Description = () => {
};
const AvataxMatcher = () => {
const router = useRouter();
const { notifyError } = useDashboardNotification();
const { isLoading } = trpcClient.avataxConnection.verifyConnections.useQuery(undefined, {
onError: () => {
notifyError("Error", "You must configure Avatax first.");
router.push("/configuration");
},
});
if (isLoading) {
return (
<Box>
<Text>Loading...</Text>
</Box>
);
}
return (
<AppColumns top={<Header />}>
<Description />
@ -52,7 +76,4 @@ const AvataxMatcher = () => {
);
};
/*
* todo: add redirect if no connection
*/
export default AvataxMatcher;

View file

@ -83,7 +83,12 @@ const ProviderCard = ({
</Box>
<Box display={"flex"} justifyContent={"flex-end"} marginTop={12}>
{!isComingSoon && (
<Button onClick={() => router.push(`/providers/${provider}`)}>Choose</Button>
<Button
data-testid="coming-soon-choose-button"
onClick={() => router.push(`/providers/${provider}`)}
>
Choose
</Button>
)}
</Box>
</AppCard>

View file

@ -1,9 +1,12 @@
import { Text } from "@saleor/macaw-ui/next";
import { Box, Text } from "@saleor/macaw-ui/next";
import { AppColumns } from "../../../modules/ui/app-columns";
import { AppDashboardLink } from "../../../modules/ui/app-dashboard-link";
import { Section } from "../../../modules/ui/app-section";
import { TextLink } from "@saleor/apps-ui";
import { TaxJarTaxCodeMatcherTable } from "../../../modules/taxjar/ui/taxjar-tax-code-matcher-table";
import { trpcClient } from "../../../modules/trpc/trpc-client";
import { useRouter } from "next/router";
import { useDashboardNotification } from "@saleor/apps-shared";
const Header = () => {
return <Section.Header>Match Saleor tax classes to TaxJar tax categories</Section.Header>;
@ -25,7 +28,10 @@ const Description = () => {
</Text>
<Text as="p" marginBottom={4}>
If you haven&apos;t created any tax classes yet, you can do it in the{" "}
<AppDashboardLink href="/taxes/tax-classes">
<AppDashboardLink
data-testid="taxjar-matcher-tax-classes-text-link"
href="/taxes/tax-classes"
>
Configuration Taxes Tax classes
</AppDashboardLink>{" "}
view.
@ -47,6 +53,24 @@ const Description = () => {
};
const TaxJarMatcher = () => {
const router = useRouter();
const { notifyError } = useDashboardNotification();
const { isLoading } = trpcClient.taxJarConnection.verifyConnections.useQuery(undefined, {
onError: () => {
notifyError("Error", "You must configure TaxJar first.");
router.push("/configuration");
},
});
if (isLoading) {
return (
<Box>
<Text>Loading...</Text>
</Box>
);
}
return (
<AppColumns top={<Header />}>
<Description />
@ -55,7 +79,4 @@ const TaxJarMatcher = () => {
);
};
/*
* todo: add redirect if no connection
*/
export default TaxJarMatcher;