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:
parent
6250095a4e
commit
d42c79f366
32 changed files with 379 additions and 160 deletions
37
apps/taxes/TESTING.md
Normal file
37
apps/taxes/TESTING.md
Normal 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.
|
|
@ -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",
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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 />
|
||||
) : (
|
||||
|
|
|
@ -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.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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",
|
||||
[
|
||||
{
|
|
@ -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;
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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={
|
||||
<>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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'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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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'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;
|
||||
|
|
Loading…
Reference in a new issue