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({
|
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",
|
||||||
|
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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>
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 />
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -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.
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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",
|
||||||
[
|
[
|
||||||
{
|
{
|
|
@ -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;
|
||||||
}
|
}
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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>
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -7,58 +7,75 @@ 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">
|
||||||
<Table.Container>
|
{isLoading ? (
|
||||||
<Table.THead>
|
<Box height="100%" display={"flex"} alignItems={"center"} justifyContent={"center"}>
|
||||||
<Table.TR>
|
<Text color="textNeutralSubdued">Loading...</Text>
|
||||||
<Table.TH>Provider</Table.TH>
|
</Box>
|
||||||
</Table.TR>
|
) : (
|
||||||
</Table.THead>
|
<>
|
||||||
<Table.TBody>
|
{isConfigured ? (
|
||||||
{isAvatax && (
|
<Table.Container>
|
||||||
<Table.TR>
|
<Table.THead>
|
||||||
<Table.TD>
|
<Table.TR>
|
||||||
<ProviderLabel name="avatax" />
|
<Table.TH>Provider</Table.TH>
|
||||||
</Table.TD>
|
</Table.TR>
|
||||||
<Table.TD>
|
</Table.THead>
|
||||||
<Box display="flex" justifyContent={"flex-end"}>
|
<Table.TBody>
|
||||||
<Button
|
{isAvatax && (
|
||||||
onClick={() => router.push("/providers/avatax/matcher")}
|
<Table.TR>
|
||||||
variant="tertiary"
|
<Table.TD>
|
||||||
>
|
<ProviderLabel name="avatax" />
|
||||||
Configure
|
</Table.TD>
|
||||||
</Button>{" "}
|
<Table.TD>
|
||||||
</Box>{" "}
|
<Box display="flex" justifyContent={"flex-end"}>
|
||||||
</Table.TD>
|
<Button
|
||||||
</Table.TR>
|
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>
|
</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={
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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't created any tax classes yet, you can do it in the{" "}
|
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
|
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;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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't created any tax classes yet, you can do it in the{" "}
|
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
|
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;
|
||||||
|
|
Loading…
Reference in a new issue