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({ 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 }) => { getById: protectedWithConfigurationService.input(getInputSchema).query(async ({ ctx, input }) => {
const logger = createLogger({ const logger = createLogger({
name: "avataxConnectionRouter.get", name: "avataxConnectionRouter.get",

View file

@ -35,4 +35,12 @@ export class PublicAvataxConnectionService {
async delete(id: string) { async delete(id: string) {
return this.connectionService.delete(id); 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; onSubmit: (data: AvataxConfig) => void;
defaultValues: AvataxConfig; defaultValues: AvataxConfig;
isLoading: boolean; isLoading: boolean;
cancelButton: React.ReactNode; leftButton: React.ReactNode;
}; };
export const AvataxConfigurationForm = (props: AvataxConfigurationFormProps) => { export const AvataxConfigurationForm = (props: AvataxConfigurationFormProps) => {
@ -48,7 +48,7 @@ export const AvataxConfigurationForm = (props: AvataxConfigurationFormProps) =>
<ProviderLabel name="avatax" /> <ProviderLabel name="avatax" />
</Box> </Box>
<form onSubmit={handleSubmit(submitHandler)}> <form onSubmit={handleSubmit(submitHandler)} data-testid="avatax-configuration-form">
<Input <Input
control={control} control={control}
name="name" name="name"
@ -211,9 +211,14 @@ export const AvataxConfigurationForm = (props: AvataxConfigurationFormProps) =>
<Divider marginY={8} /> <Divider marginY={8} />
<Box display={"flex"} justifyContent={"space-between"} alignItems={"center"}> <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"} {props.isLoading ? "Saving..." : "Save"}
</Button> </Button>
</Box> </Box>

View file

@ -5,10 +5,46 @@ import { Table } from "../../ui/table";
import { Select } from "../../ui/_select"; import { Select } from "../../ui/_select";
import { Box, Text } from "@saleor/macaw-ui/next"; import { Box, Text } from "@saleor/macaw-ui/next";
import { AppCard } from "../../ui/app-card"; 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 SelectTaxCode = ({ taxClassId }: { taxClassId: string }) => {
const [value, setValue] = React.useState(""); const [value, setValue] = React.useState("");
const { notifySuccess, notifyError } = useDashboardNotification(); const { notifySuccess, notifyError } = useDashboardNotification();
const { data: taxCodes = [], isLoading: isCodesLoading } = useGetTaxCodes();
const { data: avataxMatches, isLoading: isMatchesLoading } = const { data: avataxMatches, isLoading: isMatchesLoading } =
trpcClient.avataxMatches.getAll.useQuery(); 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) => { const changeValue = (avataxTaxCode: string) => {
setValue(avataxTaxCode); setValue(avataxTaxCode);
updateMutation({ updateMutation({

View file

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

View file

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

View file

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

View file

@ -6,6 +6,7 @@ const Intro = () => {
return ( return (
<Section.Description <Section.Description
title="Available channels" title="Available channels"
data-testid="channel-intro"
description={ description={
<> <>
This table displays all the channels configured to use the tax app as the tax calculation This table displays all the channels configured to use the tax app as the tax calculation
@ -13,7 +14,10 @@ const Intro = () => {
<br /> <br />
<br /> <br />
You can change the tax configuration method for each channel in the{" "} 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"} justifyContent={"center"}
> >
<Text variant="body">No providers configured yet</Text> <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> </Box>
); );
}; };
@ -40,7 +42,7 @@ export const ProvidersList = () => {
const isNoResult = isFetched && !isProvider; const isNoResult = isFetched && !isProvider;
return ( return (
<AppCard __minHeight={"320px"} height="100%"> <AppCard __minHeight={"320px"} height="100%" data-testid="providers-list">
{isFetching ? ( {isFetching ? (
<Skeleton /> <Skeleton />
) : ( ) : (
@ -55,7 +57,12 @@ export const ProvidersList = () => {
> >
<ProvidersTable /> <ProvidersTable />
<Box display={"flex"} justifyContent={"flex-end"}> <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>
</Box> </Box>
)} )}

View file

@ -6,6 +6,7 @@ const Intro = () => {
return ( return (
<Section.Description <Section.Description
title="Tax providers" title="Tax providers"
data-testid="providers-intro"
description={ description={
<> <>
Saleor offers two ways of calculating taxes: flat or dynamic rates. 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 { MetadataItem } from "../../../generated/graphql";
import { ChannelsConfig } from "../channel-configuration/channel-config"; import { ChannelsConfig } from "../channel-configuration/channel-config";
import { ProviderConnections } from "../provider-connections/provider-connections"; 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"; import { AuthData } from "@saleor/app-sdk/APL";
const mockedInvalidMetadata: MetadataItem[] = [ const mockedInvalidMetadata: MetadataItem[] = [
@ -95,22 +95,22 @@ const mockedAuthData: AuthData = {
vi.stubEnv("SECRET_KEY", mockedSecretKey); vi.stubEnv("SECRET_KEY", mockedSecretKey);
describe("getActiveConnection", () => { describe("getActiveConnectionService", () => {
it("throws error when channel slug is missing", () => { 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" "Channel slug was not found in the webhook payload"
); );
}); });
it("throws error when there are no metadata items", () => { 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" "App encryptedMetadata was not found in the webhook payload"
); );
}); });
it("throws error when no providerConnectionId was found", () => { it("throws error when no providerConnectionId was found", () => {
expect(() => expect(() =>
getActiveConnection( getActiveConnectionService(
"default-channel", "default-channel",
[ [
{ {
@ -129,7 +129,7 @@ describe("getActiveConnection", () => {
it("throws error when no channel was found for channelSlug", () => { it("throws error when no channel was found for channelSlug", () => {
expect(() => expect(() =>
getActiveConnection( getActiveConnectionService(
"invalid-channel", "invalid-channel",
[ [
{ {
@ -147,7 +147,7 @@ describe("getActiveConnection", () => {
}); });
it("returns provider when data is correct", () => { it("returns provider when data is correct", () => {
const result = getActiveConnection( const result = getActiveConnectionService(
"default-channel", "default-channel",
[ [
{ {

View file

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

View file

@ -35,4 +35,12 @@ export class PublicTaxJarConnectionService {
async delete(id: string) { async delete(id: string) {
return this.connectionService.delete(id); 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 * @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) { async validateAddress({ params }: ValidateAddressArgs) {
// return this.client.validateAddress(params); return this.client.validateAddress(params);
} }
async getTaxCodes() { async getTaxCodes() {

View file

@ -35,6 +35,19 @@ const protectedWithConfigurationService = protectedClientProcedure.use(({ next,
); );
export const taxjarConnectionRouter = router({ 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 }) => { getById: protectedWithConfigurationService.input(getInputSchema).query(async ({ ctx, input }) => {
const logger = createLogger({ const logger = createLogger({
name: "taxjarConnectionRouter.get", name: "taxjarConnectionRouter.get",

View file

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

View file

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

View file

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

View file

@ -5,6 +5,41 @@ import { Table } from "../../ui/table";
import { Select } from "../../ui/_select"; import { Select } from "../../ui/_select";
import { Box, Text } from "@saleor/macaw-ui/next"; import { Box, Text } from "@saleor/macaw-ui/next";
import { AppCard } from "../../ui/app-card"; 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 SelectTaxCode = ({ taxClassId }: { taxClassId: string }) => {
const [value, setValue] = React.useState(""); const [value, setValue] = React.useState("");
@ -32,23 +67,7 @@ const SelectTaxCode = ({ taxClassId }: { taxClassId: string }) => {
}, },
}); });
const { data: providers } = trpcClient.providersConfiguration.getAll.useQuery(); const { data: taxCodes = [], isLoading: isCodesLoading } = useGetTaxCodes();
/*
* 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 changeValue = (taxJarTaxCode: string) => { const changeValue = (taxJarTaxCode: string) => {
setValue(taxJarTaxCode); setValue(taxJarTaxCode);

View file

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

View file

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

View file

@ -7,15 +7,23 @@ import { Table } from "./table";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
const MatcherTable = () => { 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 isAvatax = connections.some(({ provider }) => provider === "avatax");
const isTaxJar = connections.some(({ provider }) => provider === "taxjar"); const isTaxJar = connections.some(({ provider }) => provider === "taxjar");
const isConfigured = isAvatax || isTaxJar;
const router = useRouter(); const router = useRouter();
return ( return (
<AppCard __minHeight={"320px"} height="100%"> <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.Container>
<Table.THead> <Table.THead>
<Table.TR> <Table.TR>
@ -31,6 +39,7 @@ const MatcherTable = () => {
<Table.TD> <Table.TD>
<Box display="flex" justifyContent={"flex-end"}> <Box display="flex" justifyContent={"flex-end"}>
<Button <Button
data-testid="avatax-matcher-configure-button"
onClick={() => router.push("/providers/avatax/matcher")} onClick={() => router.push("/providers/avatax/matcher")}
variant="tertiary" variant="tertiary"
> >
@ -48,6 +57,7 @@ const MatcherTable = () => {
<Table.TD> <Table.TD>
<Box display="flex" justifyContent={"flex-end"}> <Box display="flex" justifyContent={"flex-end"}>
<Button <Button
data-testid="taxjar-matcher-configure-button"
onClick={() => router.push("/providers/taxjar/matcher")} onClick={() => router.push("/providers/taxjar/matcher")}
variant="tertiary" variant="tertiary"
> >
@ -59,6 +69,13 @@ const MatcherTable = () => {
)} )}
</Table.TBody> </Table.TBody>
</Table.Container> </Table.Container>
) : (
<Box height="100%" display={"flex"} alignItems={"center"} justifyContent={"center"}>
<Text color="textNeutralSubdued">You must configure a tax provider first</Text>
</Box>
)}
</>
)}
</AppCard> </AppCard>
); );
}; };
@ -66,6 +83,7 @@ const MatcherTable = () => {
const Intro = () => { const Intro = () => {
return ( return (
<Section.Description <Section.Description
data-testid="matcher-intro"
title="Tax code matcher" title="Tax code matcher"
description={ description={
<> <>

View file

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

View file

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

View file

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

View file

@ -9,7 +9,7 @@ import {
} from "../../../../generated/graphql"; } from "../../../../generated/graphql";
import { saleorApp } from "../../../../saleor-app"; import { saleorApp } from "../../../../saleor-app";
import { createLogger } from "../../../lib/logger"; 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 { Client } from "urql";
import { WebhookResponse } from "../../../modules/app/webhook-response"; import { WebhookResponse } from "../../../modules/app/webhook-response";
import { PROVIDER_ORDER_ID_KEY } from "../../../modules/avatax/order-fulfilled/avatax-order-fulfilled-payload-transformer"; 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 { saleorApiUrl, token } = authData;
const webhookResponse = new WebhookResponse(res); const webhookResponse = new WebhookResponse(res);
logger.info({ payload }, "Handler called with payload"); logger.info("Handler called with payload");
try { try {
const appMetadata = payload.recipient?.privateMetadata ?? []; const appMetadata = payload.recipient?.privateMetadata ?? [];
const channelSlug = payload.order?.channel.slug; 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 // todo: figure out what fields are needed and add validation
if (!payload.order) { if (!payload.order) {

View file

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

View file

@ -1,6 +1,5 @@
import { ChannelSection } from "../modules/channel-configuration/ui/channel-section"; import { ChannelSection } from "../modules/channel-configuration/ui/channel-section";
import { ProvidersSection } from "../modules/provider-connections/ui/providers-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 { AppColumns } from "../modules/ui/app-columns";
import { Section } from "../modules/ui/app-section"; import { Section } from "../modules/ui/app-section";
import { MatcherSection } from "../modules/ui/matcher-section"; import { MatcherSection } from "../modules/ui/matcher-section";
@ -14,14 +13,11 @@ const Header = () => {
}; };
const ConfigurationPage = () => { const ConfigurationPage = () => {
const { data: providers = [] } = trpcClient.providersConfiguration.getAll.useQuery();
const isProviders = providers.length > 0;
return ( return (
<AppColumns top={<Header />}> <AppColumns top={<Header />}>
<ProvidersSection /> <ProvidersSection />
<ChannelSection /> <ChannelSection />
{isProviders && <MatcherSection />} <MatcherSection />
</AppColumns> </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 { AvataxTaxCodeMatcherTable } from "../../../modules/avatax/ui/avatax-tax-code-matcher-table";
import { AppColumns } from "../../../modules/ui/app-columns"; import { AppColumns } from "../../../modules/ui/app-columns";
import { AppDashboardLink } from "../../../modules/ui/app-dashboard-link"; import { AppDashboardLink } from "../../../modules/ui/app-dashboard-link";
import { Section } from "../../../modules/ui/app-section"; import { Section } from "../../../modules/ui/app-section";
import { TextLink } from "@saleor/apps-ui"; 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 = () => { const Header = () => {
return <Section.Header>Match Saleor tax classes to Avatax tax codes</Section.Header>; return <Section.Header>Match Saleor tax classes to Avatax tax codes</Section.Header>;
@ -25,7 +28,10 @@ const Description = () => {
</Text> </Text>
<Text as="p" marginBottom={4}> <Text as="p" marginBottom={4}>
If you haven&apos;t created any tax classes yet, you can do it in the{" "} 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 Configuration Taxes Tax classes
</AppDashboardLink>{" "} </AppDashboardLink>{" "}
view. view.
@ -44,6 +50,24 @@ const Description = () => {
}; };
const AvataxMatcher = () => { 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 ( return (
<AppColumns top={<Header />}> <AppColumns top={<Header />}>
<Description /> <Description />
@ -52,7 +76,4 @@ const AvataxMatcher = () => {
); );
}; };
/*
* todo: add redirect if no connection
*/
export default AvataxMatcher; export default AvataxMatcher;

View file

@ -83,7 +83,12 @@ const ProviderCard = ({
</Box> </Box>
<Box display={"flex"} justifyContent={"flex-end"} marginTop={12}> <Box display={"flex"} justifyContent={"flex-end"} marginTop={12}>
{!isComingSoon && ( {!isComingSoon && (
<Button onClick={() => router.push(`/providers/${provider}`)}>Choose</Button> <Button
data-testid="coming-soon-choose-button"
onClick={() => router.push(`/providers/${provider}`)}
>
Choose
</Button>
)} )}
</Box> </Box>
</AppCard> </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 { AppColumns } from "../../../modules/ui/app-columns";
import { AppDashboardLink } from "../../../modules/ui/app-dashboard-link"; import { AppDashboardLink } from "../../../modules/ui/app-dashboard-link";
import { Section } from "../../../modules/ui/app-section"; import { Section } from "../../../modules/ui/app-section";
import { TextLink } from "@saleor/apps-ui"; import { TextLink } from "@saleor/apps-ui";
import { TaxJarTaxCodeMatcherTable } from "../../../modules/taxjar/ui/taxjar-tax-code-matcher-table"; 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 = () => { const Header = () => {
return <Section.Header>Match Saleor tax classes to TaxJar tax categories</Section.Header>; return <Section.Header>Match Saleor tax classes to TaxJar tax categories</Section.Header>;
@ -25,7 +28,10 @@ const Description = () => {
</Text> </Text>
<Text as="p" marginBottom={4}> <Text as="p" marginBottom={4}>
If you haven&apos;t created any tax classes yet, you can do it in the{" "} 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 Configuration Taxes Tax classes
</AppDashboardLink>{" "} </AppDashboardLink>{" "}
view. view.
@ -47,6 +53,24 @@ const Description = () => {
}; };
const TaxJarMatcher = () => { 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 ( return (
<AppColumns top={<Header />}> <AppColumns top={<Header />}>
<Description /> <Description />
@ -55,7 +79,4 @@ const TaxJarMatcher = () => {
); );
}; };
/*
* todo: add redirect if no connection
*/
export default TaxJarMatcher; export default TaxJarMatcher;