Compare commits
16 commits
main
...
feat/confi
Author | SHA1 | Date | |
---|---|---|---|
![]() |
dc2483b85a | ||
![]() |
9d5636defc | ||
![]() |
ded37d61c3 | ||
![]() |
4a34a4b0c6 | ||
![]() |
bac7d970b8 | ||
![]() |
d31d698de6 | ||
![]() |
1528b60a3f | ||
![]() |
4e5f6c2d46 | ||
![]() |
371cfb721b | ||
![]() |
f1162eac50 | ||
![]() |
7e49c4fdbe | ||
![]() |
25151ca5cd | ||
![]() |
3a22d0af45 | ||
![]() |
82b2dc60af | ||
![]() |
33d227acf0 | ||
![]() |
0b54a01cbf |
87 changed files with 4280 additions and 6556 deletions
|
@ -17,13 +17,10 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^2.9.10",
|
"@hookform/resolvers": "^2.9.10",
|
||||||
"@material-ui/core": "^4.12.4",
|
|
||||||
"@material-ui/icons": "^4.11.3",
|
|
||||||
"@material-ui/lab": "4.0.0-alpha.61",
|
|
||||||
"@saleor/app-sdk": "0.39.1",
|
"@saleor/app-sdk": "0.39.1",
|
||||||
"@saleor/apps-shared": "workspace:*",
|
"@saleor/apps-shared": "workspace:*",
|
||||||
"@saleor/apps-ui": "workspace:*",
|
"@saleor/apps-ui": "workspace:*",
|
||||||
"@saleor/macaw-ui": "^0.7.2",
|
"@saleor/macaw-ui": "^0.8.0-pre.72",
|
||||||
"@sentry/nextjs": "^7.45.0",
|
"@sentry/nextjs": "^7.45.0",
|
||||||
"@tanstack/react-query": "^4.19.1",
|
"@tanstack/react-query": "^4.19.1",
|
||||||
"@trpc/client": "^10.9.0",
|
"@trpc/client": "^10.9.0",
|
||||||
|
@ -64,9 +61,9 @@
|
||||||
"@testing-library/react": "^13.4.0",
|
"@testing-library/react": "^13.4.0",
|
||||||
"@testing-library/react-hooks": "^8.0.1",
|
"@testing-library/react-hooks": "^8.0.1",
|
||||||
"@types/node": "^18.8.1",
|
"@types/node": "^18.8.1",
|
||||||
"@types/react": "^18.0.21",
|
"@types/react": "~18.0.38",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@types/react-dom": "^18.0.6",
|
||||||
"@vitejs/plugin-react": "^3.1.0",
|
"@vitejs/plugin-react": "^4.0.0",
|
||||||
"eslint": "8.25.0",
|
"eslint": "8.25.0",
|
||||||
"eslint-config-next": "12.3.1",
|
"eslint-config-next": "12.3.1",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
export { default as AvataxIcon } from "./avatax-icon.svg";
|
export { default as AvataxIcon } from "./avatax-icon.svg";
|
||||||
export { default as TaxJarIcon } from "./taxjar-icon.svg";
|
export { default as TaxJarIcon } from "./taxjar-icon.svg";
|
||||||
|
export { default as StripeTaxIcon } from "./stripe-icon.svg";
|
||||||
|
|
9
apps/taxes/src/assets/stripe-icon.svg
Normal file
9
apps/taxes/src/assets/stripe-icon.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 234 KiB |
|
@ -1,8 +0,0 @@
|
||||||
import React, { PropsWithChildren } from "react";
|
|
||||||
import dynamic from "next/dynamic";
|
|
||||||
|
|
||||||
const Wrapper = (props: PropsWithChildren<{}>) => <React.Fragment>{props.children}</React.Fragment>;
|
|
||||||
|
|
||||||
export const NoSSRWrapper = dynamic(() => Promise.resolve(Wrapper), {
|
|
||||||
ssr: false,
|
|
||||||
});
|
|
|
@ -1,48 +0,0 @@
|
||||||
import { describe, expect, it, vi } from "vitest";
|
|
||||||
import { AppBridgeState } from "@saleor/app-sdk/app-bridge";
|
|
||||||
import { render, waitFor } from "@testing-library/react";
|
|
||||||
import { ThemeSynchronizer } from "./theme-synchronizer";
|
|
||||||
|
|
||||||
const appBridgeState: AppBridgeState = {
|
|
||||||
ready: true,
|
|
||||||
token: "token",
|
|
||||||
domain: "some-domain.saleor.cloud",
|
|
||||||
theme: "dark",
|
|
||||||
path: "/",
|
|
||||||
locale: "en",
|
|
||||||
id: "app-id",
|
|
||||||
saleorApiUrl: "https://some-domain.saleor.cloud/graphql/",
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockThemeChange = vi.fn();
|
|
||||||
|
|
||||||
vi.mock("@saleor/app-sdk/app-bridge", () => {
|
|
||||||
return {
|
|
||||||
useAppBridge() {
|
|
||||||
return {
|
|
||||||
appBridgeState: appBridgeState,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
vi.mock("@saleor/macaw-ui", () => {
|
|
||||||
return {
|
|
||||||
useTheme() {
|
|
||||||
return {
|
|
||||||
setTheme: mockThemeChange,
|
|
||||||
themeType: "light",
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("ThemeSynchronizer", () => {
|
|
||||||
it("Updates MacawUI theme when AppBridgeState theme changes", () => {
|
|
||||||
render(<ThemeSynchronizer />);
|
|
||||||
|
|
||||||
return waitFor(() => {
|
|
||||||
expect(mockThemeChange).toHaveBeenCalledWith("dark");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,28 +1,25 @@
|
||||||
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
|
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
|
||||||
import { useTheme } from "@saleor/macaw-ui";
|
import { useTheme } from "@saleor/macaw-ui/next";
|
||||||
import { memo, useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
/**
|
// todo move to shared
|
||||||
* Macaw-ui stores its theme mode in memory and local storage. To synchronize App with Dashboard,
|
export function ThemeSynchronizer() {
|
||||||
* Macaw must be informed about this change from AppBridge.
|
|
||||||
*
|
|
||||||
* If you are not using Macaw, you can remove this.
|
|
||||||
*/
|
|
||||||
function _ThemeSynchronizer() {
|
|
||||||
const { appBridgeState } = useAppBridge();
|
const { appBridgeState } = useAppBridge();
|
||||||
const { setTheme, themeType } = useTheme();
|
const { setTheme } = useTheme();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!setTheme || !appBridgeState?.theme) {
|
if (!setTheme || !appBridgeState?.theme) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (themeType !== appBridgeState?.theme) {
|
if (appBridgeState.theme === "light") {
|
||||||
setTheme(appBridgeState.theme);
|
setTheme("defaultLight");
|
||||||
}
|
}
|
||||||
}, [appBridgeState?.theme, setTheme, themeType]);
|
|
||||||
|
if (appBridgeState.theme === "dark") {
|
||||||
|
setTheme("defaultDark");
|
||||||
|
}
|
||||||
|
}, [appBridgeState?.theme, setTheme]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ThemeSynchronizer = memo(_ThemeSynchronizer);
|
|
||||||
|
|
|
@ -15,9 +15,18 @@ const mockedProviders: ProvidersConfig = [
|
||||||
isAutocommit: false,
|
isAutocommit: false,
|
||||||
isSandbox: true,
|
isSandbox: true,
|
||||||
name: "avatax-1",
|
name: "avatax-1",
|
||||||
password: "avatax-password",
|
|
||||||
username: "avatax-username",
|
|
||||||
shippingTaxCode: "FR000000",
|
shippingTaxCode: "FR000000",
|
||||||
|
credentials: {
|
||||||
|
password: "avatax-password",
|
||||||
|
username: "avatax-username",
|
||||||
|
},
|
||||||
|
address: {
|
||||||
|
city: "New York",
|
||||||
|
country: "US",
|
||||||
|
state: "NY",
|
||||||
|
street: "123 Main St",
|
||||||
|
zip: "10001",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -25,8 +34,17 @@ const mockedProviders: ProvidersConfig = [
|
||||||
id: "2",
|
id: "2",
|
||||||
config: {
|
config: {
|
||||||
name: "taxjar-1",
|
name: "taxjar-1",
|
||||||
apiKey: "taxjar-api-key",
|
|
||||||
isSandbox: true,
|
isSandbox: true,
|
||||||
|
credentials: {
|
||||||
|
apiKey: "taxjar-api-key",
|
||||||
|
},
|
||||||
|
address: {
|
||||||
|
city: "New York",
|
||||||
|
country: "US",
|
||||||
|
state: "NY",
|
||||||
|
street: "123 Main St",
|
||||||
|
zip: "10001",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -34,14 +52,6 @@ const mockedEncryptedProviders = encrypt(JSON.stringify(mockedProviders), mocked
|
||||||
|
|
||||||
const mockedChannels: ChannelsConfig = {
|
const mockedChannels: ChannelsConfig = {
|
||||||
"default-channel": {
|
"default-channel": {
|
||||||
address: {
|
|
||||||
city: "New York",
|
|
||||||
country: "US",
|
|
||||||
state: "NY",
|
|
||||||
street: "123 Main St",
|
|
||||||
zip: "10001",
|
|
||||||
},
|
|
||||||
enabled: true,
|
|
||||||
providerInstanceId: "1",
|
providerInstanceId: "1",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { AddressLocationInfo as AvataxAddress } from "avatax/lib/models/AddressLocationInfo";
|
import { AddressLocationInfo as AvataxAddress } from "avatax/lib/models/AddressLocationInfo";
|
||||||
import { ChannelAddress } from "../channels-configuration/channels-config";
|
import { AvataxConfig } from "./avatax-config";
|
||||||
import { AddressFragment as SaleorAddress } from "../../../generated/graphql";
|
import { AddressFragment } from "../../../generated/graphql";
|
||||||
|
|
||||||
function mapSaleorAddressToAvataxAddress(address: SaleorAddress): AvataxAddress {
|
function mapSaleorAddressToAvataxAddress(address: AddressFragment): AvataxAddress {
|
||||||
return {
|
return {
|
||||||
line1: address.streetAddress1,
|
line1: address.streetAddress1,
|
||||||
line2: address.streetAddress2,
|
line2: address.streetAddress2,
|
||||||
|
@ -13,7 +13,7 @@ function mapSaleorAddressToAvataxAddress(address: SaleorAddress): AvataxAddress
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapChannelAddressToAvataxAddress(address: ChannelAddress): AvataxAddress {
|
function mapChannelAddressToAvataxAddress(address: AvataxConfig["address"]): AvataxAddress {
|
||||||
return {
|
return {
|
||||||
line1: address.street,
|
line1: address.street,
|
||||||
city: address.city,
|
city: address.city,
|
||||||
|
|
|
@ -59,13 +59,8 @@ export class AvataxClient {
|
||||||
constructor(config: AvataxConfig) {
|
constructor(config: AvataxConfig) {
|
||||||
this.logger = createLogger({ service: "AvataxClient" });
|
this.logger = createLogger({ service: "AvataxClient" });
|
||||||
this.logger.trace("AvataxClient constructor");
|
this.logger.trace("AvataxClient constructor");
|
||||||
const { username, password } = config;
|
|
||||||
const credentials = {
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
};
|
|
||||||
const settings = createAvataxSettings(config);
|
const settings = createAvataxSettings(config);
|
||||||
const avataxClient = new Avatax(settings).withSecurity(credentials);
|
const avataxClient = new Avatax(settings).withSecurity(config.credentials);
|
||||||
|
|
||||||
this.logger.trace({ client: avataxClient }, "External Avatax client created");
|
this.logger.trace({ client: avataxClient }, "External Avatax client created");
|
||||||
this.client = avataxClient;
|
this.client = avataxClient;
|
||||||
|
|
|
@ -1,26 +1,48 @@
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { obfuscateSecret } from "../../lib/utils";
|
import { obfuscateSecret } from "../../lib/utils";
|
||||||
|
|
||||||
export const avataxConfigSchema = z.object({
|
const addressSchema = z.object({
|
||||||
name: z.string().min(1, { message: "Name requires at least one character." }),
|
country: z.string(),
|
||||||
|
zip: z.string(),
|
||||||
|
state: z.string(),
|
||||||
|
city: z.string(),
|
||||||
|
street: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const avataxCredentialsSchema = z.object({
|
||||||
username: z.string().min(1, { message: "Username requires at least one character." }),
|
username: z.string().min(1, { message: "Username requires at least one character." }),
|
||||||
password: z.string().min(1, { message: "Password requires at least one character." }),
|
password: z.string().min(1, { message: "Password requires at least one character." }),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const avataxConfigSchema = z.object({
|
||||||
|
name: z.string().min(1, { message: "Name requires at least one character." }),
|
||||||
isSandbox: z.boolean(),
|
isSandbox: z.boolean(),
|
||||||
companyCode: z.string().optional(),
|
companyCode: z.string().optional(),
|
||||||
isAutocommit: z.boolean(),
|
isAutocommit: z.boolean(),
|
||||||
shippingTaxCode: z.string().optional(),
|
shippingTaxCode: z.string().optional(),
|
||||||
|
credentials: avataxCredentialsSchema,
|
||||||
|
address: addressSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type AvataxConfig = z.infer<typeof avataxConfigSchema>;
|
export type AvataxConfig = z.infer<typeof avataxConfigSchema>;
|
||||||
|
|
||||||
export const defaultAvataxConfig: AvataxConfig = {
|
export const defaultAvataxConfig: AvataxConfig = {
|
||||||
name: "",
|
name: "",
|
||||||
username: "",
|
|
||||||
password: "",
|
|
||||||
companyCode: "",
|
companyCode: "",
|
||||||
isSandbox: true,
|
isSandbox: true,
|
||||||
isAutocommit: false,
|
isAutocommit: false,
|
||||||
shippingTaxCode: "",
|
shippingTaxCode: "",
|
||||||
|
credentials: {
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
},
|
||||||
|
address: {
|
||||||
|
city: "",
|
||||||
|
country: "",
|
||||||
|
state: "",
|
||||||
|
street: "",
|
||||||
|
zip: "",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const avataxInstanceConfigSchema = z.object({
|
export const avataxInstanceConfigSchema = z.object({
|
||||||
|
@ -31,13 +53,18 @@ export const avataxInstanceConfigSchema = z.object({
|
||||||
|
|
||||||
export type AvataxInstanceConfig = z.infer<typeof avataxInstanceConfigSchema>;
|
export type AvataxInstanceConfig = z.infer<typeof avataxInstanceConfigSchema>;
|
||||||
|
|
||||||
export const obfuscateAvataxConfig = (config: AvataxConfig) => ({
|
export const obfuscateAvataxConfig = (config: AvataxConfig): AvataxConfig => ({
|
||||||
...config,
|
...config,
|
||||||
username: obfuscateSecret(config.username),
|
credentials: {
|
||||||
password: obfuscateSecret(config.password),
|
...config.credentials,
|
||||||
|
username: obfuscateSecret(config.credentials.username),
|
||||||
|
password: obfuscateSecret(config.credentials.password),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const obfuscateAvataxInstances = (instances: AvataxInstanceConfig[]) =>
|
export const obfuscateAvataxInstances = (
|
||||||
|
instances: AvataxInstanceConfig[]
|
||||||
|
): AvataxInstanceConfig[] =>
|
||||||
instances.map((instance) => ({
|
instances.map((instance) => ({
|
||||||
...instance,
|
...instance,
|
||||||
config: obfuscateAvataxConfig(instance.config),
|
config: obfuscateAvataxConfig(instance.config),
|
||||||
|
|
|
@ -16,13 +16,16 @@ const deleteInputSchema = z.object({
|
||||||
|
|
||||||
const patchInputSchema = z.object({
|
const patchInputSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
value: avataxConfigSchema.partial().transform((c) => {
|
value: avataxConfigSchema.partial().transform((config) => {
|
||||||
const { username, password, ...config } = c ?? {};
|
const { username, password } = config.credentials ?? {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...config,
|
...config,
|
||||||
...(username && !isObfuscated(username) && { username }),
|
credentials: {
|
||||||
...(password && !isObfuscated(password) && { password }),
|
...config.credentials,
|
||||||
|
...(username && !isObfuscated(username) && { username }),
|
||||||
|
...(password && !isObfuscated(password) && { password }),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { providersSchema } from "../providers-configuration/providers-config";
|
||||||
import { TAX_PROVIDER_KEY } from "../providers-configuration/public-providers-configuration-service";
|
import { TAX_PROVIDER_KEY } from "../providers-configuration/public-providers-configuration-service";
|
||||||
import { AvataxClient } from "./avatax-client";
|
import { AvataxClient } from "./avatax-client";
|
||||||
import { AvataxConfig, AvataxInstanceConfig, avataxInstanceConfigSchema } from "./avatax-config";
|
import { AvataxConfig, AvataxInstanceConfig, avataxInstanceConfigSchema } from "./avatax-config";
|
||||||
|
import { DeepPartial } from "@trpc/server";
|
||||||
|
|
||||||
const getSchema = avataxInstanceConfigSchema;
|
const getSchema = avataxInstanceConfigSchema;
|
||||||
|
|
||||||
|
@ -77,7 +78,7 @@ export class AvataxConfigurationService {
|
||||||
return result.data;
|
return result.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async patch(id: string, config: Partial<AvataxConfig>): Promise<void> {
|
async patch(id: string, config: DeepPartial<AvataxConfig>): Promise<void> {
|
||||||
this.logger.debug(`.patch called with id: ${id} and value: ${JSON.stringify(config)}`);
|
this.logger.debug(`.patch called with id: ${id} and value: ${JSON.stringify(config)}`);
|
||||||
const data = await this.get(id);
|
const data = await this.get(id);
|
||||||
// omit the key "id" from the result
|
// omit the key "id" from the result
|
||||||
|
|
|
@ -4,7 +4,6 @@ import {
|
||||||
TaxBaseFragment,
|
TaxBaseFragment,
|
||||||
} from "../../../generated/graphql";
|
} from "../../../generated/graphql";
|
||||||
import { Logger, createLogger } from "../../lib/logger";
|
import { Logger, createLogger } from "../../lib/logger";
|
||||||
import { ChannelConfig } from "../channels-configuration/channels-config";
|
|
||||||
import { ProviderWebhookService } from "../taxes/tax-provider-webhook";
|
import { ProviderWebhookService } from "../taxes/tax-provider-webhook";
|
||||||
import { AvataxClient } from "./avatax-client";
|
import { AvataxClient } from "./avatax-client";
|
||||||
import { AvataxConfig, defaultAvataxConfig } from "./avatax-config";
|
import { AvataxConfig, defaultAvataxConfig } from "./avatax-config";
|
||||||
|
@ -29,31 +28,35 @@ export class AvataxWebhookService implements ProviderWebhookService {
|
||||||
this.client = avataxClient;
|
this.client = avataxClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
async calculateTaxes(taxBase: TaxBaseFragment, channelConfig: ChannelConfig) {
|
async calculateTaxes(taxBase: TaxBaseFragment) {
|
||||||
this.logger.debug({ taxBase, channelConfig }, "calculateTaxes called with:");
|
this.logger.debug({ taxBase }, "calculateTaxes called with:");
|
||||||
const adapter = new AvataxCalculateTaxesAdapter(this.config);
|
const adapter = new AvataxCalculateTaxesAdapter(this.config);
|
||||||
|
|
||||||
const response = await adapter.send({ channelConfig, taxBase });
|
const response = await adapter.send({ taxBase });
|
||||||
|
|
||||||
this.logger.debug({ response }, "calculateTaxes response:");
|
this.logger.debug({ response }, "calculateTaxes response:");
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOrder(order: OrderCreatedSubscriptionFragment, channelConfig: ChannelConfig) {
|
async createOrder(order: OrderCreatedSubscriptionFragment) {
|
||||||
this.logger.debug({ order, channelConfig }, "createOrder called with:");
|
const providerConfig = this.config;
|
||||||
|
|
||||||
|
this.logger.debug({ order, providerConfig }, "createOrder called with:");
|
||||||
|
|
||||||
const adapter = new AvataxOrderCreatedAdapter(this.config);
|
const adapter = new AvataxOrderCreatedAdapter(this.config);
|
||||||
|
|
||||||
const response = await adapter.send({ channelConfig, order });
|
const response = await adapter.send({ order });
|
||||||
|
|
||||||
this.logger.debug({ response }, "createOrder response:");
|
this.logger.debug({ response }, "createOrder response:");
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fulfillOrder(order: OrderFulfilledSubscriptionFragment, channelConfig: ChannelConfig) {
|
async fulfillOrder(order: OrderFulfilledSubscriptionFragment) {
|
||||||
this.logger.debug({ order, channelConfig }, "fulfillOrder called with:");
|
const providerConfig = this.config;
|
||||||
|
|
||||||
|
this.logger.debug({ order, providerConfig }, "fulfillOrder called with:");
|
||||||
|
|
||||||
const adapter = new AvataxOrderFulfilledAdapter(this.config);
|
const adapter = new AvataxOrderFulfilledAdapter(this.config);
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { TaxBaseFragment } from "../../../../generated/graphql";
|
import { TaxBaseFragment } from "../../../../generated/graphql";
|
||||||
import { Logger, createLogger } from "../../../lib/logger";
|
import { Logger, createLogger } from "../../../lib/logger";
|
||||||
import { ChannelConfig } from "../../channels-configuration/channels-config";
|
|
||||||
import { CalculateTaxesResponse } from "../../taxes/tax-provider-webhook";
|
import { CalculateTaxesResponse } from "../../taxes/tax-provider-webhook";
|
||||||
import { WebhookAdapter } from "../../taxes/tax-webhook-adapter";
|
import { WebhookAdapter } from "../../taxes/tax-webhook-adapter";
|
||||||
import { AvataxClient, CreateTransactionArgs } from "../avatax-client";
|
import { AvataxClient, CreateTransactionArgs } from "../avatax-client";
|
||||||
|
@ -10,25 +9,25 @@ import { AvataxCalculateTaxesResponseTransformer } from "./avatax-calculate-taxe
|
||||||
|
|
||||||
export const SHIPPING_ITEM_CODE = "Shipping";
|
export const SHIPPING_ITEM_CODE = "Shipping";
|
||||||
|
|
||||||
export type Payload = {
|
export type AvataxCalculateTaxesPayload = {
|
||||||
taxBase: TaxBaseFragment;
|
taxBase: TaxBaseFragment;
|
||||||
channelConfig: ChannelConfig;
|
|
||||||
config: AvataxConfig;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Target = CreateTransactionArgs;
|
export type AvataxCalculateTaxesTarget = CreateTransactionArgs;
|
||||||
export type Response = CalculateTaxesResponse;
|
export type AvataxCalculateTaxesResponse = CalculateTaxesResponse;
|
||||||
|
|
||||||
export class AvataxCalculateTaxesAdapter implements WebhookAdapter<Payload, Response> {
|
export class AvataxCalculateTaxesAdapter
|
||||||
|
implements WebhookAdapter<AvataxCalculateTaxesPayload, AvataxCalculateTaxesResponse>
|
||||||
|
{
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
constructor(private readonly config: AvataxConfig) {
|
constructor(private readonly config: AvataxConfig) {
|
||||||
this.logger = createLogger({ service: "AvataxCalculateTaxesAdapter" });
|
this.logger = createLogger({ service: "AvataxCalculateTaxesAdapter" });
|
||||||
}
|
}
|
||||||
|
|
||||||
async send(payload: Pick<Payload, "channelConfig" | "taxBase">): Promise<Response> {
|
async send(payload: AvataxCalculateTaxesPayload): Promise<AvataxCalculateTaxesResponse> {
|
||||||
this.logger.debug({ payload }, "send called with:");
|
this.logger.debug({ payload }, "send called with:");
|
||||||
const payloadTransformer = new AvataxCalculateTaxesPayloadTransformer();
|
const payloadTransformer = new AvataxCalculateTaxesPayloadTransformer();
|
||||||
const target = payloadTransformer.transform({ ...payload, config: this.config });
|
const target = payloadTransformer.transform({ ...payload, providerConfig: this.config });
|
||||||
|
|
||||||
const client = new AvataxClient(this.config);
|
const client = new AvataxClient(this.config);
|
||||||
const response = await client.createTransaction(target);
|
const response = await client.createTransaction(target);
|
||||||
|
|
|
@ -111,14 +111,6 @@ const defaultTaxBase: TaxBase = {
|
||||||
|
|
||||||
const defaultChannelConfig: ChannelConfig = {
|
const defaultChannelConfig: ChannelConfig = {
|
||||||
providerInstanceId: "b8c29f49-7cae-4762-8458-e9a27eb83081",
|
providerInstanceId: "b8c29f49-7cae-4762-8458-e9a27eb83081",
|
||||||
enabled: false,
|
|
||||||
address: {
|
|
||||||
country: "US",
|
|
||||||
zip: "92093",
|
|
||||||
state: "CA",
|
|
||||||
city: "La Jolla",
|
|
||||||
street: "9500 Gilman Drive",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultTransactionModel: TransactionModel = {
|
const defaultTransactionModel: TransactionModel = {
|
||||||
|
@ -948,9 +940,18 @@ const defaultAvataxConfig: AvataxConfig = {
|
||||||
isAutocommit: false,
|
isAutocommit: false,
|
||||||
isSandbox: true,
|
isSandbox: true,
|
||||||
name: "Avatax-1",
|
name: "Avatax-1",
|
||||||
password: "password",
|
|
||||||
username: "username",
|
|
||||||
shippingTaxCode: "FR000000",
|
shippingTaxCode: "FR000000",
|
||||||
|
address: {
|
||||||
|
country: "US",
|
||||||
|
zip: "92093",
|
||||||
|
state: "CA",
|
||||||
|
city: "La Jolla",
|
||||||
|
street: "9500 Gilman Drive",
|
||||||
|
},
|
||||||
|
credentials: {
|
||||||
|
password: "password",
|
||||||
|
username: "username",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const testingScenariosMap = {
|
const testingScenariosMap = {
|
||||||
|
|
|
@ -13,8 +13,7 @@ describe("AvataxCalculateTaxesPayloadTransformer", () => {
|
||||||
|
|
||||||
const payload = new AvataxCalculateTaxesPayloadTransformer().transform({
|
const payload = new AvataxCalculateTaxesPayloadTransformer().transform({
|
||||||
taxBase: taxBaseMock,
|
taxBase: taxBaseMock,
|
||||||
channelConfig: mockGenerator.generateChannelConfig(),
|
providerConfig: avataxConfigMock,
|
||||||
config: avataxConfigMock,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(payload.model.discount).toEqual(10);
|
expect(payload.model.discount).toEqual(10);
|
||||||
|
@ -26,8 +25,7 @@ describe("AvataxCalculateTaxesPayloadTransformer", () => {
|
||||||
|
|
||||||
const payload = new AvataxCalculateTaxesPayloadTransformer().transform({
|
const payload = new AvataxCalculateTaxesPayloadTransformer().transform({
|
||||||
taxBase: taxBaseMock,
|
taxBase: taxBaseMock,
|
||||||
channelConfig: mockGenerator.generateChannelConfig(),
|
providerConfig: avataxConfigMock,
|
||||||
config: avataxConfigMock,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(payload.model.discount).toEqual(0);
|
expect(payload.model.discount).toEqual(0);
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
|
import { DocumentType } from "avatax/lib/enums/DocumentType";
|
||||||
import { LineItemModel } from "avatax/lib/models/LineItemModel";
|
import { LineItemModel } from "avatax/lib/models/LineItemModel";
|
||||||
import { TaxBaseFragment } from "../../../../generated/graphql";
|
import { TaxBaseFragment } from "../../../../generated/graphql";
|
||||||
import { AvataxConfig } from "../avatax-config";
|
|
||||||
import { avataxAddressFactory } from "../address-factory";
|
|
||||||
import { DocumentType } from "avatax/lib/enums/DocumentType";
|
|
||||||
import { SHIPPING_ITEM_CODE, Payload, Target } from "./avatax-calculate-taxes-adapter";
|
|
||||||
import { discountUtils } from "../../taxes/discount-utils";
|
import { discountUtils } from "../../taxes/discount-utils";
|
||||||
|
import { avataxAddressFactory } from "../address-factory";
|
||||||
|
import { CreateTransactionArgs } from "../avatax-client";
|
||||||
|
import { AvataxConfig } from "../avatax-config";
|
||||||
|
import { SHIPPING_ITEM_CODE } from "./avatax-calculate-taxes-adapter";
|
||||||
|
|
||||||
export function mapPayloadLines(taxBase: TaxBaseFragment, config: AvataxConfig): LineItemModel[] {
|
export function mapPayloadLines(taxBase: TaxBaseFragment, config: AvataxConfig): LineItemModel[] {
|
||||||
const isDiscounted = taxBase.discounts.length > 0;
|
const isDiscounted = taxBase.discounts.length > 0;
|
||||||
|
@ -35,22 +36,26 @@ export function mapPayloadLines(taxBase: TaxBaseFragment, config: AvataxConfig):
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AvataxCalculateTaxesPayloadTransformer {
|
export class AvataxCalculateTaxesPayloadTransformer {
|
||||||
transform(props: Payload): Target {
|
transform({
|
||||||
const { taxBase, channelConfig, config } = props;
|
taxBase,
|
||||||
|
providerConfig,
|
||||||
|
}: {
|
||||||
|
taxBase: TaxBaseFragment;
|
||||||
|
providerConfig: AvataxConfig;
|
||||||
|
}): CreateTransactionArgs {
|
||||||
return {
|
return {
|
||||||
model: {
|
model: {
|
||||||
type: DocumentType.SalesOrder,
|
type: DocumentType.SalesOrder,
|
||||||
customerCode: taxBase.sourceObject.user?.id ?? "",
|
customerCode: taxBase.sourceObject.user?.id ?? "",
|
||||||
companyCode: config.companyCode,
|
companyCode: providerConfig.companyCode,
|
||||||
// * commit: If true, the transaction will be committed immediately after it is created. See: https://developer.avalara.com/communications/dev-guide_rest_v2/commit-uncommit
|
// * commit: If true, the transaction will be committed immediately after it is created. See: https://developer.avalara.com/communications/dev-guide_rest_v2/commit-uncommit
|
||||||
commit: config.isAutocommit,
|
commit: providerConfig.isAutocommit,
|
||||||
addresses: {
|
addresses: {
|
||||||
shipFrom: avataxAddressFactory.fromChannelAddress(channelConfig.address),
|
shipFrom: avataxAddressFactory.fromChannelAddress(providerConfig.address),
|
||||||
shipTo: avataxAddressFactory.fromSaleorAddress(taxBase.address!),
|
shipTo: avataxAddressFactory.fromSaleorAddress(taxBase.address!),
|
||||||
},
|
},
|
||||||
currencyCode: taxBase.currency,
|
currencyCode: taxBase.currency,
|
||||||
lines: mapPayloadLines(taxBase, config),
|
lines: mapPayloadLines(taxBase, providerConfig),
|
||||||
date: new Date(),
|
date: new Date(),
|
||||||
discount: discountUtils.sumDiscounts(
|
discount: discountUtils.sumDiscounts(
|
||||||
taxBase.discounts.map((discount) => discount.amount.amount)
|
taxBase.discounts.map((discount) => discount.amount.amount)
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import { TransactionModel } from "avatax/lib/models/TransactionModel";
|
import { TransactionModel } from "avatax/lib/models/TransactionModel";
|
||||||
import { numbers } from "../../taxes/numbers";
|
import { numbers } from "../../taxes/numbers";
|
||||||
import { taxProviderUtils } from "../../taxes/tax-provider-utils";
|
import { taxProviderUtils } from "../../taxes/tax-provider-utils";
|
||||||
import { Response, SHIPPING_ITEM_CODE } from "./avatax-calculate-taxes-adapter";
|
import { CalculateTaxesResponse } from "../../taxes/tax-provider-webhook";
|
||||||
|
import { SHIPPING_ITEM_CODE } from "./avatax-calculate-taxes-adapter";
|
||||||
|
|
||||||
export class AvataxCalculateTaxesResponseLinesTransformer {
|
export class AvataxCalculateTaxesResponseLinesTransformer {
|
||||||
transform(transaction: TransactionModel): Response["lines"] {
|
transform(transaction: TransactionModel): CalculateTaxesResponse["lines"] {
|
||||||
const productLines = transaction.lines?.filter((line) => line.itemCode !== SHIPPING_ITEM_CODE);
|
const productLines = transaction.lines?.filter((line) => line.itemCode !== SHIPPING_ITEM_CODE);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import { TransactionModel } from "avatax/lib/models/TransactionModel";
|
import { TransactionModel } from "avatax/lib/models/TransactionModel";
|
||||||
import { numbers } from "../../taxes/numbers";
|
import { numbers } from "../../taxes/numbers";
|
||||||
import { taxProviderUtils } from "../../taxes/tax-provider-utils";
|
import { taxProviderUtils } from "../../taxes/tax-provider-utils";
|
||||||
import { Response, SHIPPING_ITEM_CODE } from "./avatax-calculate-taxes-adapter";
|
import { CalculateTaxesResponse } from "../../taxes/tax-provider-webhook";
|
||||||
|
import { SHIPPING_ITEM_CODE } from "./avatax-calculate-taxes-adapter";
|
||||||
|
|
||||||
export class AvataxCalculateTaxesResponseShippingTransformer {
|
export class AvataxCalculateTaxesResponseShippingTransformer {
|
||||||
transform(
|
transform(
|
||||||
transaction: TransactionModel
|
transaction: TransactionModel
|
||||||
): Pick<
|
): Pick<
|
||||||
Response,
|
CalculateTaxesResponse,
|
||||||
"shipping_price_gross_amount" | "shipping_price_net_amount" | "shipping_tax_rate"
|
"shipping_price_gross_amount" | "shipping_price_net_amount" | "shipping_tax_rate"
|
||||||
> {
|
> {
|
||||||
const shippingLine = transaction.lines?.find((line) => line.itemCode === SHIPPING_ITEM_CODE);
|
const shippingLine = transaction.lines?.find((line) => line.itemCode === SHIPPING_ITEM_CODE);
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { TransactionModel } from "avatax/lib/models/TransactionModel";
|
import { TransactionModel } from "avatax/lib/models/TransactionModel";
|
||||||
import { Response } from "./avatax-calculate-taxes-adapter";
|
import { CalculateTaxesResponse } from "../../taxes/tax-provider-webhook";
|
||||||
import { AvataxCalculateTaxesResponseLinesTransformer } from "./avatax-calculate-taxes-response-lines-transformer";
|
import { AvataxCalculateTaxesResponseLinesTransformer } from "./avatax-calculate-taxes-response-lines-transformer";
|
||||||
import { AvataxCalculateTaxesResponseShippingTransformer } from "./avatax-calculate-taxes-response-shipping-transformer";
|
import { AvataxCalculateTaxesResponseShippingTransformer } from "./avatax-calculate-taxes-response-shipping-transformer";
|
||||||
|
|
||||||
export class AvataxCalculateTaxesResponseTransformer {
|
export class AvataxCalculateTaxesResponseTransformer {
|
||||||
transform(response: TransactionModel): Response {
|
transform(response: TransactionModel): CalculateTaxesResponse {
|
||||||
const shippingTransformer = new AvataxCalculateTaxesResponseShippingTransformer();
|
const shippingTransformer = new AvataxCalculateTaxesResponseShippingTransformer();
|
||||||
const shipping = shippingTransformer.transform(response);
|
const shipping = shippingTransformer.transform(response);
|
||||||
|
|
||||||
|
|
|
@ -1,33 +1,31 @@
|
||||||
import { OrderCreatedSubscriptionFragment } from "../../../../generated/graphql";
|
import { OrderCreatedSubscriptionFragment } from "../../../../generated/graphql";
|
||||||
import { ChannelConfig } from "../../channels-configuration/channels-config";
|
import { Logger, createLogger } from "../../../lib/logger";
|
||||||
import { CreateOrderResponse } from "../../taxes/tax-provider-webhook";
|
import { CreateOrderResponse } from "../../taxes/tax-provider-webhook";
|
||||||
import { WebhookAdapter } from "../../taxes/tax-webhook-adapter";
|
import { WebhookAdapter } from "../../taxes/tax-webhook-adapter";
|
||||||
import { AvataxClient, CreateTransactionArgs } from "../avatax-client";
|
import { AvataxClient } from "../avatax-client";
|
||||||
import { AvataxConfig } from "../avatax-config";
|
import { AvataxConfig } from "../avatax-config";
|
||||||
import { AvataxOrderCreatedResponseTransformer } from "./avatax-order-created-response-transformer";
|
|
||||||
import { AvataxOrderCreatedPayloadTransformer } from "./avatax-order-created-payload-transformer";
|
import { AvataxOrderCreatedPayloadTransformer } from "./avatax-order-created-payload-transformer";
|
||||||
import { Logger, createLogger } from "../../../lib/logger";
|
import { AvataxOrderCreatedResponseTransformer } from "./avatax-order-created-response-transformer";
|
||||||
|
|
||||||
export type Payload = {
|
type AvataxOrderCreatedPayload = {
|
||||||
order: OrderCreatedSubscriptionFragment;
|
order: OrderCreatedSubscriptionFragment;
|
||||||
channelConfig: ChannelConfig;
|
|
||||||
config: AvataxConfig;
|
|
||||||
};
|
};
|
||||||
export type Target = CreateTransactionArgs;
|
type AvataxOrderCreatedResponse = CreateOrderResponse;
|
||||||
type Response = CreateOrderResponse;
|
|
||||||
|
|
||||||
export class AvataxOrderCreatedAdapter implements WebhookAdapter<Payload, Response> {
|
export class AvataxOrderCreatedAdapter
|
||||||
|
implements WebhookAdapter<AvataxOrderCreatedPayload, AvataxOrderCreatedResponse>
|
||||||
|
{
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
|
||||||
constructor(private readonly config: AvataxConfig) {
|
constructor(private readonly config: AvataxConfig) {
|
||||||
this.logger = createLogger({ service: "AvataxOrderCreatedAdapter" });
|
this.logger = createLogger({ service: "AvataxOrderCreatedAdapter" });
|
||||||
}
|
}
|
||||||
|
|
||||||
async send(payload: Pick<Payload, "channelConfig" | "order">): Promise<Response> {
|
async send(payload: AvataxOrderCreatedPayload): Promise<AvataxOrderCreatedResponse> {
|
||||||
this.logger.debug({ payload }, "send called with:");
|
this.logger.debug({ payload }, "send called with:");
|
||||||
|
|
||||||
const payloadTransformer = new AvataxOrderCreatedPayloadTransformer();
|
const payloadTransformer = new AvataxOrderCreatedPayloadTransformer(this.config);
|
||||||
const target = payloadTransformer.transform({ ...payload, config: this.config });
|
const target = payloadTransformer.transform(payload);
|
||||||
|
|
||||||
const client = new AvataxClient(this.config);
|
const client = new AvataxClient(this.config);
|
||||||
const response = await client.createTransaction(target);
|
const response = await client.createTransaction(target);
|
||||||
|
|
|
@ -7,14 +7,6 @@ import { defaultOrder } from "../../../mocks";
|
||||||
|
|
||||||
const defaultChannelConfig: ChannelConfig = {
|
const defaultChannelConfig: ChannelConfig = {
|
||||||
providerInstanceId: "aa5293e5-7f5d-4782-a619-222ead918e50",
|
providerInstanceId: "aa5293e5-7f5d-4782-a619-222ead918e50",
|
||||||
enabled: false,
|
|
||||||
address: {
|
|
||||||
country: "US",
|
|
||||||
zip: "95008",
|
|
||||||
state: "CA",
|
|
||||||
city: "Campbell",
|
|
||||||
street: "33 N. First Street",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultOrderCreatedResponse: TransactionModel = orderCreatedTransactionMock;
|
const defaultOrderCreatedResponse: TransactionModel = orderCreatedTransactionMock;
|
||||||
|
@ -24,9 +16,18 @@ const defaultAvataxConfig: AvataxConfig = {
|
||||||
isAutocommit: false,
|
isAutocommit: false,
|
||||||
isSandbox: true,
|
isSandbox: true,
|
||||||
name: "Avatax-1",
|
name: "Avatax-1",
|
||||||
password: "password",
|
|
||||||
username: "username",
|
|
||||||
shippingTaxCode: "FR000000",
|
shippingTaxCode: "FR000000",
|
||||||
|
address: {
|
||||||
|
country: "US",
|
||||||
|
zip: "95008",
|
||||||
|
state: "CA",
|
||||||
|
city: "Campbell",
|
||||||
|
street: "33 N. First Street",
|
||||||
|
},
|
||||||
|
credentials: {
|
||||||
|
password: "password",
|
||||||
|
username: "username",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const testingScenariosMap = {
|
const testingScenariosMap = {
|
||||||
|
|
|
@ -22,10 +22,10 @@ const channelConfigMock = mockGenerator.generateChannelConfig();
|
||||||
|
|
||||||
describe("AvataxOrderCreatedPayloadTransformer", () => {
|
describe("AvataxOrderCreatedPayloadTransformer", () => {
|
||||||
it("returns lines with discounted: true when there are discounts", () => {
|
it("returns lines with discounted: true when there are discounts", () => {
|
||||||
const transformer = new AvataxOrderCreatedPayloadTransformer();
|
const transformer = new AvataxOrderCreatedPayloadTransformer(avataxConfigMock);
|
||||||
const payloadMock = {
|
const payloadMock = {
|
||||||
order: discountedOrderMock,
|
order: discountedOrderMock,
|
||||||
config: avataxConfigMock,
|
providerConfig: avataxConfigMock,
|
||||||
channelConfig: channelConfigMock,
|
channelConfig: channelConfigMock,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -37,10 +37,10 @@ describe("AvataxOrderCreatedPayloadTransformer", () => {
|
||||||
expect(check).toBe(true);
|
expect(check).toBe(true);
|
||||||
});
|
});
|
||||||
it("returns lines with discounted: false when there are no discounts", () => {
|
it("returns lines with discounted: false when there are no discounts", () => {
|
||||||
const transformer = new AvataxOrderCreatedPayloadTransformer();
|
const transformer = new AvataxOrderCreatedPayloadTransformer(avataxConfigMock);
|
||||||
const payloadMock = {
|
const payloadMock = {
|
||||||
order: orderMock,
|
order: orderMock,
|
||||||
config: avataxConfigMock,
|
providerConfig: avataxConfigMock,
|
||||||
channelConfig: channelConfigMock,
|
channelConfig: channelConfigMock,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,8 @@ import { numbers } from "../../taxes/numbers";
|
||||||
import { AvataxConfig } from "../avatax-config";
|
import { AvataxConfig } from "../avatax-config";
|
||||||
import { avataxAddressFactory } from "../address-factory";
|
import { avataxAddressFactory } from "../address-factory";
|
||||||
import { DocumentType } from "avatax/lib/enums/DocumentType";
|
import { DocumentType } from "avatax/lib/enums/DocumentType";
|
||||||
import { Payload, Target } from "./avatax-order-created-adapter";
|
|
||||||
import { discountUtils } from "../../taxes/discount-utils";
|
import { discountUtils } from "../../taxes/discount-utils";
|
||||||
|
import { CreateTransactionArgs } from "../avatax-client";
|
||||||
|
|
||||||
const SHIPPING_ITEM_CODE = "Shipping";
|
const SHIPPING_ITEM_CODE = "Shipping";
|
||||||
|
|
||||||
|
@ -49,24 +49,25 @@ export function mapLines(
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AvataxOrderCreatedPayloadTransformer {
|
export class AvataxOrderCreatedPayloadTransformer {
|
||||||
transform = ({ order, channelConfig, config }: Payload): Target => {
|
constructor(private readonly providerConfig: AvataxConfig) {}
|
||||||
|
transform = ({ order }: { order: OrderCreatedSubscriptionFragment }): CreateTransactionArgs => {
|
||||||
return {
|
return {
|
||||||
model: {
|
model: {
|
||||||
type: DocumentType.SalesInvoice,
|
type: DocumentType.SalesInvoice,
|
||||||
customerCode:
|
customerCode:
|
||||||
order.user?.id ??
|
order.user?.id ??
|
||||||
"" /* In Saleor Avatax plugin, the customer code is 0. In Taxes App, we set it to the user id. */,
|
"" /* In Saleor Avatax plugin, the customer code is 0. In Taxes App, we set it to the user id. */,
|
||||||
companyCode: config.companyCode,
|
companyCode: this.providerConfig.companyCode,
|
||||||
// * commit: If true, the transaction will be committed immediately after it is created. See: https://developer.avalara.com/communications/dev-guide_rest_v2/commit-uncommit
|
// * commit: If true, the transaction will be committed immediately after it is created. See: https://developer.avalara.com/communications/dev-guide_rest_v2/commit-uncommit
|
||||||
commit: config.isAutocommit,
|
commit: this.providerConfig.isAutocommit,
|
||||||
addresses: {
|
addresses: {
|
||||||
shipFrom: avataxAddressFactory.fromChannelAddress(channelConfig.address),
|
shipFrom: avataxAddressFactory.fromChannelAddress(this.providerConfig.address),
|
||||||
// billing or shipping address?
|
// billing or shipping address?
|
||||||
shipTo: avataxAddressFactory.fromSaleorAddress(order.billingAddress!),
|
shipTo: avataxAddressFactory.fromSaleorAddress(order.billingAddress!),
|
||||||
},
|
},
|
||||||
currencyCode: order.total.currency,
|
currencyCode: order.total.currency,
|
||||||
email: order.user?.email ?? "",
|
email: order.user?.email ?? "",
|
||||||
lines: mapLines(order, config),
|
lines: mapLines(order, this.providerConfig),
|
||||||
date: new Date(order.created),
|
date: new Date(order.created),
|
||||||
discount: discountUtils.sumDiscounts(
|
discount: discountUtils.sumDiscounts(
|
||||||
order.discounts.map((discount) => discount.amount.amount)
|
order.discounts.map((discount) => discount.amount.amount)
|
||||||
|
|
|
@ -6,25 +6,26 @@ import { AvataxConfig } from "../avatax-config";
|
||||||
import { AvataxOrderFulfilledPayloadTransformer } from "./avatax-order-fulfilled-payload-transformer";
|
import { AvataxOrderFulfilledPayloadTransformer } from "./avatax-order-fulfilled-payload-transformer";
|
||||||
import { AvataxOrderFulfilledResponseTransformer } from "./avatax-order-fulfilled-response-transformer";
|
import { AvataxOrderFulfilledResponseTransformer } from "./avatax-order-fulfilled-response-transformer";
|
||||||
|
|
||||||
export type Payload = {
|
export type AvataxOrderFulfilledPayload = {
|
||||||
order: OrderFulfilledSubscriptionFragment;
|
order: OrderFulfilledSubscriptionFragment;
|
||||||
config: AvataxConfig;
|
|
||||||
};
|
};
|
||||||
export type Target = CommitTransactionArgs;
|
export type AvataxOrderFulfilledTarget = CommitTransactionArgs;
|
||||||
export type Response = { ok: true };
|
export type AvataxOrderFulfilledResponse = { ok: true };
|
||||||
|
|
||||||
export class AvataxOrderFulfilledAdapter implements WebhookAdapter<Payload, Response> {
|
export class AvataxOrderFulfilledAdapter
|
||||||
|
implements WebhookAdapter<AvataxOrderFulfilledPayload, AvataxOrderFulfilledResponse>
|
||||||
|
{
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
|
||||||
constructor(private readonly config: AvataxConfig) {
|
constructor(private readonly config: AvataxConfig) {
|
||||||
this.logger = createLogger({ service: "AvataxOrderFulfilledAdapter" });
|
this.logger = createLogger({ service: "AvataxOrderFulfilledAdapter" });
|
||||||
}
|
}
|
||||||
|
|
||||||
async send(payload: Pick<Payload, "order">): Promise<Response> {
|
async send(payload: AvataxOrderFulfilledPayload): Promise<AvataxOrderFulfilledResponse> {
|
||||||
this.logger.debug({ payload }, "send called with:");
|
this.logger.debug({ payload }, "send called with:");
|
||||||
|
|
||||||
const payloadTransformer = new AvataxOrderFulfilledPayloadTransformer();
|
const payloadTransformer = new AvataxOrderFulfilledPayloadTransformer(this.config);
|
||||||
const target = payloadTransformer.transform({ ...payload, config: this.config });
|
const target = payloadTransformer.transform({ ...payload });
|
||||||
|
|
||||||
const client = new AvataxClient(this.config);
|
const client = new AvataxClient(this.config);
|
||||||
const response = await client.commitTransaction(target);
|
const response = await client.commitTransaction(target);
|
||||||
|
|
|
@ -2,7 +2,6 @@ import { DocumentType } from "avatax/lib/enums/DocumentType";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { OrderFulfilledSubscriptionFragment } from "../../../../generated/graphql";
|
import { OrderFulfilledSubscriptionFragment } from "../../../../generated/graphql";
|
||||||
import { AvataxConfig } from "../avatax-config";
|
import { AvataxConfig } from "../avatax-config";
|
||||||
import { Payload } from "./avatax-order-fulfilled-adapter";
|
|
||||||
import {
|
import {
|
||||||
AvataxOrderFulfilledPayloadTransformer,
|
AvataxOrderFulfilledPayloadTransformer,
|
||||||
PROVIDER_ORDER_ID_KEY,
|
PROVIDER_ORDER_ID_KEY,
|
||||||
|
@ -16,9 +15,18 @@ const MOCK_AVATAX_CONFIG: AvataxConfig = {
|
||||||
isAutocommit: false,
|
isAutocommit: false,
|
||||||
isSandbox: true,
|
isSandbox: true,
|
||||||
name: "Avatax-1",
|
name: "Avatax-1",
|
||||||
password: "password",
|
|
||||||
username: "username",
|
|
||||||
shippingTaxCode: "FR000000",
|
shippingTaxCode: "FR000000",
|
||||||
|
address: {
|
||||||
|
country: "US",
|
||||||
|
zip: "10118",
|
||||||
|
state: "NY",
|
||||||
|
city: "New York",
|
||||||
|
street: "350 5th Avenue",
|
||||||
|
},
|
||||||
|
credentials: {
|
||||||
|
password: "password",
|
||||||
|
username: "username",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const MOCKED_METADATA: OrderFulfilledSubscriptionFragment["privateMetadata"] = [
|
const MOCKED_METADATA: OrderFulfilledSubscriptionFragment["privateMetadata"] = [
|
||||||
|
@ -121,11 +129,12 @@ describe("getTransactionCodeFromMetadata", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const transformer = new AvataxOrderFulfilledPayloadTransformer();
|
const transformer = new AvataxOrderFulfilledPayloadTransformer(MOCK_AVATAX_CONFIG);
|
||||||
|
|
||||||
const MOCKED_ORDER_FULFILLED_PAYLOAD: Payload = {
|
const MOCKED_ORDER_FULFILLED_PAYLOAD: {
|
||||||
|
order: OrderFulfilledSubscriptionFragment;
|
||||||
|
} = {
|
||||||
order: ORDER_FULFILLED_MOCK,
|
order: ORDER_FULFILLED_MOCK,
|
||||||
config: MOCK_AVATAX_CONFIG,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("AvataxOrderFulfilledPayloadTransformer", () => {
|
describe("AvataxOrderFulfilledPayloadTransformer", () => {
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
import { OrderFulfilledSubscriptionFragment } from "../../../../generated/graphql";
|
|
||||||
import { DocumentType } from "avatax/lib/enums/DocumentType";
|
import { DocumentType } from "avatax/lib/enums/DocumentType";
|
||||||
import { Payload, Target } from "./avatax-order-fulfilled-adapter";
|
import { OrderFulfilledSubscriptionFragment } from "../../../../generated/graphql";
|
||||||
|
import { AvataxConfig } from "../avatax-config";
|
||||||
|
import {
|
||||||
|
AvataxOrderFulfilledPayload,
|
||||||
|
AvataxOrderFulfilledTarget,
|
||||||
|
} from "./avatax-order-fulfilled-adapter";
|
||||||
|
|
||||||
// * This is the key that we use to store the provider order id in the Saleor order metadata.
|
// * This is the key that we use to store the provider order id in the Saleor order metadata.
|
||||||
|
|
||||||
|
@ -19,12 +23,13 @@ export function getTransactionCodeFromMetadata(
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AvataxOrderFulfilledPayloadTransformer {
|
export class AvataxOrderFulfilledPayloadTransformer {
|
||||||
transform({ order, config }: Payload): Target {
|
constructor(private readonly config: AvataxConfig) {}
|
||||||
|
transform({ order }: AvataxOrderFulfilledPayload): AvataxOrderFulfilledTarget {
|
||||||
const transactionCode = getTransactionCodeFromMetadata(order.privateMetadata);
|
const transactionCode = getTransactionCodeFromMetadata(order.privateMetadata);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
transactionCode,
|
transactionCode,
|
||||||
companyCode: config.companyCode ?? "",
|
companyCode: this.config.companyCode ?? "",
|
||||||
documentType: DocumentType.SalesInvoice,
|
documentType: DocumentType.SalesInvoice,
|
||||||
model: {
|
model: {
|
||||||
commit: true,
|
commit: true,
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { TransactionModel } from "avatax/lib/models/TransactionModel";
|
import { TransactionModel } from "avatax/lib/models/TransactionModel";
|
||||||
import { Response } from "./avatax-order-fulfilled-adapter";
|
import { AvataxOrderFulfilledResponse } from "./avatax-order-fulfilled-adapter";
|
||||||
|
|
||||||
export class AvataxOrderFulfilledResponseTransformer {
|
export class AvataxOrderFulfilledResponseTransformer {
|
||||||
transform(response: TransactionModel): Response {
|
transform(response: TransactionModel): AvataxOrderFulfilledResponse {
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,318 +0,0 @@
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import {
|
|
||||||
FormHelperText,
|
|
||||||
Grid,
|
|
||||||
InputLabel,
|
|
||||||
Switch,
|
|
||||||
TextField,
|
|
||||||
TextFieldProps,
|
|
||||||
} from "@material-ui/core";
|
|
||||||
import { Delete, Save } from "@material-ui/icons";
|
|
||||||
import { Button, makeStyles } from "@saleor/macaw-ui";
|
|
||||||
import React from "react";
|
|
||||||
import { Controller, useForm } from "react-hook-form";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { useInstanceId } from "../../taxes/tax-context";
|
|
||||||
import { trpcClient } from "../../trpc/trpc-client";
|
|
||||||
import { AppLink } from "../../ui/app-link";
|
|
||||||
import { avataxConfigSchema } from "../avatax-config";
|
|
||||||
import { useDashboardNotification } from "@saleor/apps-shared";
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
|
||||||
reverseRow: {
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "row-reverse",
|
|
||||||
gap: theme.spacing(1),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const schema = avataxConfigSchema;
|
|
||||||
|
|
||||||
type FormValues = z.infer<typeof schema>;
|
|
||||||
|
|
||||||
const defaultValues: FormValues = {
|
|
||||||
companyCode: "",
|
|
||||||
isAutocommit: false,
|
|
||||||
isSandbox: false,
|
|
||||||
password: "",
|
|
||||||
username: "",
|
|
||||||
name: "",
|
|
||||||
shippingTaxCode: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AvataxConfigurationForm = () => {
|
|
||||||
const { notifySuccess, notifyError } = useDashboardNotification();
|
|
||||||
const [isWarningDialogOpen, setIsWarningDialogOpen] = React.useState(false);
|
|
||||||
const styles = useStyles();
|
|
||||||
const { handleSubmit, reset, control, formState } = useForm<FormValues>({
|
|
||||||
resolver: zodResolver(schema),
|
|
||||||
defaultValues,
|
|
||||||
});
|
|
||||||
const { instanceId, setInstanceId } = useInstanceId();
|
|
||||||
const { refetch: refetchChannelConfigurationData } =
|
|
||||||
trpcClient.channelsConfiguration.fetch.useQuery(undefined, {
|
|
||||||
onError(error) {
|
|
||||||
notifyError("Error", error.message);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const { refetch: refetchProvidersConfigurationData } =
|
|
||||||
trpcClient.providersConfiguration.getAll.useQuery();
|
|
||||||
const { data: instance } = trpcClient.avataxConfiguration.get.useQuery(
|
|
||||||
{ id: instanceId ?? "" },
|
|
||||||
{
|
|
||||||
enabled: !!instanceId,
|
|
||||||
onError(error) {
|
|
||||||
notifyError("Error", error.message);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const resetInstanceId = () => {
|
|
||||||
setInstanceId(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (instance) {
|
|
||||||
const { config } = instance;
|
|
||||||
|
|
||||||
reset(config);
|
|
||||||
} else {
|
|
||||||
reset(defaultValues);
|
|
||||||
}
|
|
||||||
}, [instance, reset]);
|
|
||||||
|
|
||||||
const { mutate: createMutation, isLoading: isCreateLoading } =
|
|
||||||
trpcClient.avataxConfiguration.post.useMutation({
|
|
||||||
onSuccess({ id }) {
|
|
||||||
setInstanceId(id);
|
|
||||||
refetchProvidersConfigurationData();
|
|
||||||
notifySuccess("Success", "Saved app configuration");
|
|
||||||
},
|
|
||||||
onError(error) {
|
|
||||||
notifyError("Error", error.message);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { mutate: updateMutation, isLoading: isUpdateLoading } =
|
|
||||||
trpcClient.avataxConfiguration.patch.useMutation({
|
|
||||||
onSuccess() {
|
|
||||||
refetchProvidersConfigurationData();
|
|
||||||
notifySuccess("Success", "Updated Avalara configuration");
|
|
||||||
},
|
|
||||||
onError(error) {
|
|
||||||
notifyError("Error", error.message);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { mutate: deleteMutation } = trpcClient.avataxConfiguration.delete.useMutation({
|
|
||||||
onSuccess() {
|
|
||||||
resetInstanceId();
|
|
||||||
refetchProvidersConfigurationData();
|
|
||||||
refetchChannelConfigurationData();
|
|
||||||
notifySuccess("Success", "Removed Avatax instance");
|
|
||||||
},
|
|
||||||
onError(error) {
|
|
||||||
notifyError("Error", error.message);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const textFieldProps: TextFieldProps = {
|
|
||||||
fullWidth: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSubmit = (value: FormValues) => {
|
|
||||||
if (instanceId) {
|
|
||||||
updateMutation({
|
|
||||||
id: instanceId,
|
|
||||||
value,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
createMutation({
|
|
||||||
value,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeWarningDialog = () => {
|
|
||||||
setIsWarningDialogOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const openWarningDialog = () => {
|
|
||||||
setIsWarningDialogOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteProvider = () => {
|
|
||||||
closeWarningDialog();
|
|
||||||
if (instanceId) {
|
|
||||||
deleteMutation({ id: instanceId });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const isLoading = isCreateLoading || isUpdateLoading;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<form autoComplete="off" onSubmit={handleSubmit(onSubmit)}>
|
|
||||||
<Grid container spacing={2}>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Controller
|
|
||||||
name="name"
|
|
||||||
control={control}
|
|
||||||
defaultValue={defaultValues.name}
|
|
||||||
render={({ field }) => (
|
|
||||||
<TextField
|
|
||||||
required
|
|
||||||
type="text"
|
|
||||||
{...field}
|
|
||||||
label="Instance name"
|
|
||||||
{...textFieldProps}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{formState.errors.name && (
|
|
||||||
<FormHelperText error>{formState.errors.name.message}</FormHelperText>
|
|
||||||
)}
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<InputLabel>
|
|
||||||
Sandbox
|
|
||||||
<Controller
|
|
||||||
name={"isSandbox"}
|
|
||||||
control={control}
|
|
||||||
defaultValue={defaultValues.isSandbox}
|
|
||||||
render={({ field }) => (
|
|
||||||
<Switch
|
|
||||||
{...field}
|
|
||||||
checked={field.value}
|
|
||||||
onChange={(e) => field.onChange(e.target.checked)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</InputLabel>
|
|
||||||
<FormHelperText>
|
|
||||||
Toggling between{" "}
|
|
||||||
<AppLink
|
|
||||||
href={
|
|
||||||
"https://developer.avalara.com/erp-integration-guide/sales-tax-badge/authentication-in-avatax/sandbox-vs-production/"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<q>Production</q> and <q>Sandbox</q>
|
|
||||||
</AppLink>{" "}
|
|
||||||
environments.{" "}
|
|
||||||
</FormHelperText>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<InputLabel>
|
|
||||||
Autocommit
|
|
||||||
<Controller
|
|
||||||
name={"isAutocommit"}
|
|
||||||
control={control}
|
|
||||||
defaultValue={defaultValues.isAutocommit}
|
|
||||||
render={({ field }) => (
|
|
||||||
<Switch
|
|
||||||
{...field}
|
|
||||||
checked={field.value}
|
|
||||||
onChange={(e) => field.onChange(e.target.checked)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</InputLabel>
|
|
||||||
<FormHelperText>
|
|
||||||
If enabled, the order will be automatically{" "}
|
|
||||||
<AppLink
|
|
||||||
href={
|
|
||||||
"https://developer.avalara.com/communications/dev-guide_rest_v2/commit-uncommit/"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
committed to Avalara.
|
|
||||||
</AppLink>{" "}
|
|
||||||
</FormHelperText>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Controller
|
|
||||||
name="username"
|
|
||||||
control={control}
|
|
||||||
defaultValue=""
|
|
||||||
render={({ field }) => (
|
|
||||||
<TextField required type="text" {...field} label="Username" {...textFieldProps} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{formState.errors.username && (
|
|
||||||
<FormHelperText error>{formState.errors.username.message}</FormHelperText>
|
|
||||||
)}
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Controller
|
|
||||||
name="password"
|
|
||||||
control={control}
|
|
||||||
defaultValue={defaultValues.password}
|
|
||||||
render={({ field }) => (
|
|
||||||
<TextField required label="Password" {...field} {...textFieldProps} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{formState.errors.password && (
|
|
||||||
<FormHelperText error>{formState.errors.password.message}</FormHelperText>
|
|
||||||
)}
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Controller
|
|
||||||
name="companyCode"
|
|
||||||
control={control}
|
|
||||||
defaultValue={defaultValues.companyCode}
|
|
||||||
render={({ field }) => (
|
|
||||||
<TextField type="text" {...field} label="Company code" {...textFieldProps} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormHelperText>
|
|
||||||
{"When not provided, the default company will be used. "}
|
|
||||||
<AppLink href="https://developer.avalara.com/erp-integration-guide/sales-tax-badge/transactions/simple-transactions/company-codes/">
|
|
||||||
Read more
|
|
||||||
</AppLink>{" "}
|
|
||||||
about company codes.
|
|
||||||
</FormHelperText>
|
|
||||||
{formState.errors.companyCode && (
|
|
||||||
<FormHelperText error>{formState.errors.companyCode.message}</FormHelperText>
|
|
||||||
)}
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Controller
|
|
||||||
name="shippingTaxCode"
|
|
||||||
control={control}
|
|
||||||
defaultValue={defaultValues.shippingTaxCode}
|
|
||||||
render={({ field }) => (
|
|
||||||
<TextField type="text" {...field} label="Shipping tax code" {...textFieldProps} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormHelperText>
|
|
||||||
{"Tax code that for the shipping line sent to Avatax. "}
|
|
||||||
<AppLink href="https://taxcode.avatax.avalara.com">
|
|
||||||
Must match Avatax tax codes format.
|
|
||||||
</AppLink>
|
|
||||||
</FormHelperText>
|
|
||||||
{formState.errors.shippingTaxCode && (
|
|
||||||
<FormHelperText error>{formState.errors.shippingTaxCode.message}</FormHelperText>
|
|
||||||
)}
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
<br />
|
|
||||||
<div className={styles.reverseRow}>
|
|
||||||
<Button startIcon={<Save />} type="submit" variant="primary">
|
|
||||||
{isLoading ? "Saving..." : "Save"}
|
|
||||||
</Button>
|
|
||||||
{instanceId && (
|
|
||||||
<Button onClick={deleteProvider} startIcon={<Delete />}>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{/* <DeleteProviderDialog
|
|
||||||
isOpen={isWarningDialogOpen}
|
|
||||||
onClose={closeWarningDialog}
|
|
||||||
onCancel={closeWarningDialog}
|
|
||||||
onConfirm={deleteProvider}
|
|
||||||
/> */}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,10 +0,0 @@
|
||||||
import { AvataxConfigurationForm } from "./avatax-configuration-form";
|
|
||||||
|
|
||||||
export const AvataxConfiguration = () => {
|
|
||||||
return (
|
|
||||||
<section>
|
|
||||||
<h2>Avatax configuration</h2>
|
|
||||||
<AvataxConfigurationForm />
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,20 +1,8 @@
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { ChannelFragment } from "../../../generated/graphql";
|
import { ChannelFragment } from "../../../generated/graphql";
|
||||||
|
|
||||||
const addressSchema = z.object({
|
|
||||||
country: z.string(),
|
|
||||||
zip: z.string(),
|
|
||||||
state: z.string(),
|
|
||||||
city: z.string(),
|
|
||||||
street: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type ChannelAddress = z.infer<typeof addressSchema>;
|
|
||||||
|
|
||||||
export const channelSchema = z.object({
|
export const channelSchema = z.object({
|
||||||
providerInstanceId: z.string(),
|
providerInstanceId: z.string(),
|
||||||
enabled: z.boolean(),
|
|
||||||
address: addressSchema,
|
|
||||||
});
|
});
|
||||||
export type ChannelConfig = z.infer<typeof channelSchema>;
|
export type ChannelConfig = z.infer<typeof channelSchema>;
|
||||||
|
|
||||||
|
@ -23,14 +11,6 @@ export type ChannelsConfig = z.infer<typeof channelsSchema>;
|
||||||
|
|
||||||
export const defaultChannelConfig: ChannelConfig = {
|
export const defaultChannelConfig: ChannelConfig = {
|
||||||
providerInstanceId: "",
|
providerInstanceId: "",
|
||||||
address: {
|
|
||||||
city: "",
|
|
||||||
country: "",
|
|
||||||
state: "",
|
|
||||||
street: "",
|
|
||||||
zip: "",
|
|
||||||
},
|
|
||||||
enabled: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createDefaultChannelsConfig = (channels: ChannelFragment[]): ChannelsConfig => {
|
export const createDefaultChannelsConfig = (channels: ChannelFragment[]): ChannelsConfig => {
|
||||||
|
|
|
@ -1,218 +0,0 @@
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import {
|
|
||||||
FormGroup,
|
|
||||||
FormHelperText,
|
|
||||||
Grid,
|
|
||||||
InputLabel,
|
|
||||||
MenuItem,
|
|
||||||
Select,
|
|
||||||
TextField,
|
|
||||||
TextFieldProps,
|
|
||||||
Typography,
|
|
||||||
} from "@material-ui/core";
|
|
||||||
import { Save } from "@material-ui/icons";
|
|
||||||
import { Button, makeStyles } from "@saleor/macaw-ui";
|
|
||||||
|
|
||||||
import { useDashboardNotification } from "@saleor/apps-shared";
|
|
||||||
import React from "react";
|
|
||||||
import { Controller, useForm } from "react-hook-form";
|
|
||||||
import {
|
|
||||||
ChannelConfig,
|
|
||||||
channelSchema,
|
|
||||||
defaultChannelConfig,
|
|
||||||
} from "../../channels-configuration/channels-config";
|
|
||||||
import { ProvidersConfig } from "../../providers-configuration/providers-config";
|
|
||||||
import { ProviderIcon } from "../../providers-configuration/ui/provider-icon";
|
|
||||||
import { useChannelSlug } from "../../taxes/tax-context";
|
|
||||||
import { trpcClient } from "../../trpc/trpc-client";
|
|
||||||
import { CountrySelect } from "../../ui/country-select/country-select";
|
|
||||||
|
|
||||||
type ChannelTaxProviderFormValues = ChannelConfig;
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
|
||||||
reverseRow: {
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "row-reverse",
|
|
||||||
},
|
|
||||||
menuItem: {
|
|
||||||
display: "flex",
|
|
||||||
gap: theme.spacing(1),
|
|
||||||
alignItems: "center",
|
|
||||||
},
|
|
||||||
helperText: {
|
|
||||||
marginTop: 0,
|
|
||||||
marginBottom: theme.spacing(1),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const getDefaultFormValues = (
|
|
||||||
channel: ChannelConfig | undefined,
|
|
||||||
providers: ProvidersConfig
|
|
||||||
): ChannelTaxProviderFormValues => {
|
|
||||||
if (channel && channel.providerInstanceId !== "") {
|
|
||||||
return {
|
|
||||||
...defaultChannelConfig,
|
|
||||||
...channel,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const isOnlyOneInstance = providers.length === 1;
|
|
||||||
|
|
||||||
if (isOnlyOneInstance) {
|
|
||||||
return {
|
|
||||||
...defaultChannelConfig,
|
|
||||||
providerInstanceId: providers[0].id,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return defaultChannelConfig;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ChannelTaxProviderForm = () => {
|
|
||||||
const styles = useStyles();
|
|
||||||
const { control, reset, handleSubmit } = useForm<ChannelTaxProviderFormValues>({
|
|
||||||
resolver: zodResolver(channelSchema),
|
|
||||||
});
|
|
||||||
const { notifyError, notifySuccess } = useDashboardNotification();
|
|
||||||
|
|
||||||
const { channelSlug } = useChannelSlug();
|
|
||||||
|
|
||||||
const { data: channelConfigurationData, refetch: refetchChannelConfigurationData } =
|
|
||||||
trpcClient.channelsConfiguration.fetch.useQuery(undefined, {
|
|
||||||
onError(error) {
|
|
||||||
notifyError("Error", error.message);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data: providerInstances = [] } = trpcClient.providersConfiguration.getAll.useQuery(
|
|
||||||
undefined,
|
|
||||||
{
|
|
||||||
onError(error) {
|
|
||||||
notifyError("Error", error.message);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const channelConfig = channelConfigurationData?.[channelSlug];
|
|
||||||
|
|
||||||
const { mutate, isLoading } = trpcClient.channelsConfiguration.upsert.useMutation({
|
|
||||||
onSuccess() {
|
|
||||||
refetchChannelConfigurationData();
|
|
||||||
notifySuccess("Success", `Saved configuration of channel: ${channelSlug}`);
|
|
||||||
},
|
|
||||||
onError(error) {
|
|
||||||
notifyError("Error", error.message);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const defaultValues = getDefaultFormValues(channelConfig, providerInstances);
|
|
||||||
|
|
||||||
reset(defaultValues);
|
|
||||||
}, [channelConfig, providerInstances, reset]);
|
|
||||||
|
|
||||||
const textFieldProps: TextFieldProps = {
|
|
||||||
fullWidth: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSubmit = (values: ChannelTaxProviderFormValues) => {
|
|
||||||
mutate({
|
|
||||||
channelSlug,
|
|
||||||
config: {
|
|
||||||
...values,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
|
||||||
<Grid container spacing={4}>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<InputLabel>
|
|
||||||
Channel tax provider
|
|
||||||
<Controller
|
|
||||||
name={"providerInstanceId"}
|
|
||||||
control={control}
|
|
||||||
defaultValue={""}
|
|
||||||
render={({ field }) => (
|
|
||||||
<Select fullWidth {...field}>
|
|
||||||
{providerInstances.map(({ config, id, provider }) => (
|
|
||||||
<MenuItem value={id} key={id}>
|
|
||||||
<div className={styles.menuItem}>
|
|
||||||
<Typography variant="body1">{config.name}</Typography>
|
|
||||||
<ProviderIcon size={"medium"} provider={provider} />
|
|
||||||
</div>
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</InputLabel>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<FormGroup>
|
|
||||||
<Typography variant="h4">Ship from address</Typography>
|
|
||||||
<FormHelperText className={styles.helperText}>
|
|
||||||
The taxes will be calculated based on the address.
|
|
||||||
</FormHelperText>
|
|
||||||
|
|
||||||
<Grid container spacing={2}>
|
|
||||||
<Grid item xs={8}>
|
|
||||||
<Controller
|
|
||||||
name="address.country"
|
|
||||||
control={control}
|
|
||||||
render={({ field }) => <CountrySelect {...field} />}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={4}>
|
|
||||||
<Controller
|
|
||||||
name="address.zip"
|
|
||||||
control={control}
|
|
||||||
defaultValue=""
|
|
||||||
render={({ field }) => <TextField {...field} label="Zip" {...textFieldProps} />}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={6}>
|
|
||||||
<Controller
|
|
||||||
name="address.state"
|
|
||||||
control={control}
|
|
||||||
defaultValue=""
|
|
||||||
render={({ field }) => (
|
|
||||||
<TextField {...field} label="State" {...textFieldProps} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={6}>
|
|
||||||
<Controller
|
|
||||||
name="address.city"
|
|
||||||
control={control}
|
|
||||||
defaultValue=""
|
|
||||||
render={({ field }) => (
|
|
||||||
<TextField {...field} label="City" {...textFieldProps} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Controller
|
|
||||||
name="address.street"
|
|
||||||
control={control}
|
|
||||||
defaultValue=""
|
|
||||||
render={({ field }) => (
|
|
||||||
<TextField {...field} label="Street" {...textFieldProps} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</FormGroup>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
<br />
|
|
||||||
<div className={styles.reverseRow}>
|
|
||||||
<Button variant="primary" startIcon={<Save />} type="submit">
|
|
||||||
{isLoading ? "Saving..." : "Save"}
|
|
||||||
</Button>{" "}
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,160 +0,0 @@
|
||||||
import { Grid, Typography } from "@material-ui/core";
|
|
||||||
import { Warning } from "@material-ui/icons";
|
|
||||||
import { Skeleton } from "@material-ui/lab";
|
|
||||||
import { Button, makeStyles } from "@saleor/macaw-ui";
|
|
||||||
import { PropsWithChildren } from "react";
|
|
||||||
import { useAppRedirect } from "../../../lib/app/redirect";
|
|
||||||
import { ProviderIcon } from "../../providers-configuration/ui/provider-icon";
|
|
||||||
import { providerConfig, TaxProviderName } from "../../taxes/provider-config";
|
|
||||||
import { useActiveTab, useChannelSlug, useInstanceId } from "../../taxes/tax-context";
|
|
||||||
import { trpcClient } from "../../trpc/trpc-client";
|
|
||||||
import { AppLink } from "../../ui/app-link";
|
|
||||||
import { AppPaper } from "../../ui/app-paper";
|
|
||||||
import { ChannelTaxProviderForm } from "./channel-tax-provider-form";
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
|
||||||
centerWithGap: {
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: theme.spacing(1),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const NoDataPlaceholder = ({
|
|
||||||
title,
|
|
||||||
children,
|
|
||||||
}: PropsWithChildren<{
|
|
||||||
title: string;
|
|
||||||
}>) => {
|
|
||||||
const styles = useStyles();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AppPaper>
|
|
||||||
<div>
|
|
||||||
<span className={styles.centerWithGap}>
|
|
||||||
<Typography component={"h3"} variant="h3">
|
|
||||||
{title}
|
|
||||||
</Typography>
|
|
||||||
<Warning />
|
|
||||||
</span>
|
|
||||||
<br />
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</AppPaper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const NoChannelPlaceholder = () => {
|
|
||||||
const { redirect } = useAppRedirect();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<NoDataPlaceholder title={"Channels not found"}>
|
|
||||||
<Typography variant="body1">
|
|
||||||
For a channel to appear on this list, you need to configure it on the{" "}
|
|
||||||
<AppLink href="/taxes/channels">Tax Configuration</AppLink> page.
|
|
||||||
</Typography>
|
|
||||||
<br />
|
|
||||||
<Typography variant="body1">
|
|
||||||
By default, each channel will use <q>flat rates</q> as the tax calculation method. If you
|
|
||||||
want a channel to calculate taxes using the Tax App, you need to change the tax calculation
|
|
||||||
method to <b>Use tax app</b>.
|
|
||||||
</Typography>
|
|
||||||
<br />
|
|
||||||
<Button onClick={() => redirect("/taxes/channels")}>Go to Tax Configuration</Button>
|
|
||||||
</NoDataPlaceholder>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const NoProviderPlaceholder = () => {
|
|
||||||
const styles = useStyles();
|
|
||||||
const { setActiveTab } = useActiveTab();
|
|
||||||
const { setInstanceId } = useInstanceId();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<NoDataPlaceholder title={"Tax providers not found"}>
|
|
||||||
<Typography variant="body1">
|
|
||||||
You need to set up at least one tax provider before you can configure a channel.
|
|
||||||
</Typography>
|
|
||||||
<br />
|
|
||||||
<Typography>
|
|
||||||
We currently support the following tax providers:
|
|
||||||
<ul>
|
|
||||||
{Object.entries(providerConfig).map(([provider, { label }]) => (
|
|
||||||
<Typography variant="body1" component={"li"} key={label}>
|
|
||||||
<span className={styles.centerWithGap}>
|
|
||||||
{label}
|
|
||||||
<ProviderIcon size={"medium"} provider={provider as TaxProviderName} />
|
|
||||||
</span>
|
|
||||||
</Typography>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</Typography>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
setActiveTab("providers");
|
|
||||||
setInstanceId(null);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Configure a tax provider
|
|
||||||
</Button>
|
|
||||||
</NoDataPlaceholder>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ChannelTaxProviderSkeleton = () => {
|
|
||||||
return (
|
|
||||||
<AppPaper>
|
|
||||||
<Grid container spacing={2}>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Skeleton variant="rect" width={"35%"} height={10} />
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Skeleton variant="rect" width={"100%"} height={30} />
|
|
||||||
</Grid>
|
|
||||||
<br />
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Skeleton variant="rect" width={"35%"} height={10} />
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={8}>
|
|
||||||
<Skeleton variant="rect" width={"100%"} height={50} />
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={4}>
|
|
||||||
<Skeleton variant="rect" width={"100%"} height={50} />
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={6}>
|
|
||||||
<Skeleton variant="rect" width={"100%"} height={50} />
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={6}>
|
|
||||||
<Skeleton variant="rect" width={"100%"} height={50} />
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Skeleton variant="rect" width={"100%"} height={50} />
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</AppPaper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ChannelTaxProvider = () => {
|
|
||||||
const { channelSlug } = useChannelSlug();
|
|
||||||
const channels = trpcClient.channels.fetch.useQuery(undefined, {});
|
|
||||||
const providers = trpcClient.providersConfiguration.getAll.useQuery();
|
|
||||||
|
|
||||||
if (channels?.isFetching || providers?.isFetching) {
|
|
||||||
return <ChannelTaxProviderSkeleton />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!channelSlug) {
|
|
||||||
return <NoChannelPlaceholder />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!providers?.data?.length) {
|
|
||||||
return <NoProviderPlaceholder />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AppPaper>
|
|
||||||
<ChannelTaxProviderForm />
|
|
||||||
</AppPaper>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,70 +0,0 @@
|
||||||
import { makeStyles } from "@saleor/macaw-ui";
|
|
||||||
import {
|
|
||||||
OffsettedList,
|
|
||||||
OffsettedListBody,
|
|
||||||
OffsettedListHeader,
|
|
||||||
OffsettedListItem,
|
|
||||||
OffsettedListItemCell,
|
|
||||||
} from "@saleor/macaw-ui";
|
|
||||||
import clsx from "clsx";
|
|
||||||
import { ChannelFragment } from "../../../../generated/graphql";
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => {
|
|
||||||
return {
|
|
||||||
headerItem: {
|
|
||||||
height: "auto !important",
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "1fr",
|
|
||||||
},
|
|
||||||
listItem: {
|
|
||||||
cursor: "pointer",
|
|
||||||
height: "auto !important",
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "1fr",
|
|
||||||
},
|
|
||||||
listItemActive: {
|
|
||||||
border: `2px solid ${theme.palette.primary.main}`,
|
|
||||||
},
|
|
||||||
cellSlug: {
|
|
||||||
fontFamily: "monospace",
|
|
||||||
opacity: 0.8,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
channels: ChannelFragment[];
|
|
||||||
activeChannelSlug: string;
|
|
||||||
onChannelClick(channelSlug: string): void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ChannelsList = ({ channels, activeChannelSlug, onChannelClick }: Props) => {
|
|
||||||
const styles = useStyles();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<OffsettedList gridTemplate={["1fr", "1fr"]}>
|
|
||||||
<OffsettedListHeader>
|
|
||||||
<OffsettedListItem className={styles.headerItem}>
|
|
||||||
<OffsettedListItemCell>Channel name</OffsettedListItemCell>
|
|
||||||
</OffsettedListItem>
|
|
||||||
</OffsettedListHeader>
|
|
||||||
<OffsettedListBody>
|
|
||||||
{channels.map((c) => {
|
|
||||||
return (
|
|
||||||
<OffsettedListItem
|
|
||||||
className={clsx(styles.listItem, {
|
|
||||||
[styles.listItemActive]: c.slug === activeChannelSlug,
|
|
||||||
})}
|
|
||||||
key={c.slug}
|
|
||||||
onClick={() => {
|
|
||||||
onChannelClick(c.slug);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<OffsettedListItemCell>{c.name}</OffsettedListItemCell>
|
|
||||||
</OffsettedListItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</OffsettedListBody>
|
|
||||||
</OffsettedList>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,56 +0,0 @@
|
||||||
import { Grid } from "@material-ui/core";
|
|
||||||
import { Skeleton } from "@material-ui/lab";
|
|
||||||
import { useChannelSlug } from "../../taxes/tax-context";
|
|
||||||
import { trpcClient } from "../../trpc/trpc-client";
|
|
||||||
import { AppPaper } from "../../ui/app-paper";
|
|
||||||
import { ChannelsList } from "./channels-list";
|
|
||||||
|
|
||||||
const ChannelsSkeleton = () => {
|
|
||||||
return (
|
|
||||||
<AppPaper>
|
|
||||||
<Grid container spacing={2}>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Skeleton variant="rect" width={"45%"} height={10} />
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Skeleton variant="rect" width={"100%"} height={30} />
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</AppPaper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Channels = () => {
|
|
||||||
const { channelSlug, setChannelSlug } = useChannelSlug();
|
|
||||||
|
|
||||||
const channels = trpcClient.channels.fetch.useQuery(undefined, {
|
|
||||||
onSuccess: (result) => {
|
|
||||||
if (result?.[0]) {
|
|
||||||
setChannelSlug(result?.[0].slug);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (channels?.isFetching) {
|
|
||||||
return <ChannelsSkeleton />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (channels.error) {
|
|
||||||
return <div>Error. No channel available</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (channels.data?.length === 0) {
|
|
||||||
// empty space for grid
|
|
||||||
return <div></div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AppPaper>
|
|
||||||
<ChannelsList
|
|
||||||
channels={channels.data ?? []}
|
|
||||||
activeChannelSlug={channelSlug}
|
|
||||||
onChannelClick={(nextSlug) => setChannelSlug(nextSlug)}
|
|
||||||
/>
|
|
||||||
</AppPaper>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -7,3 +7,4 @@ export const providersSchema = z.array(providerSchema);
|
||||||
|
|
||||||
export type ProvidersConfig = z.infer<typeof providersSchema>;
|
export type ProvidersConfig = z.infer<typeof providersSchema>;
|
||||||
export type ProviderConfig = z.infer<typeof providerSchema>;
|
export type ProviderConfig = z.infer<typeof providerSchema>;
|
||||||
|
export type ProviderName = ProviderConfig["provider"];
|
||||||
|
|
|
@ -1,105 +0,0 @@
|
||||||
import { FormControlLabel, Grid, Radio, RadioGroup, Typography } from "@material-ui/core";
|
|
||||||
import { makeStyles } from "@saleor/macaw-ui";
|
|
||||||
import React from "react";
|
|
||||||
import { AvataxConfiguration } from "../../avatax/ui/avatax-configuration";
|
|
||||||
import { providerConfig, TaxProviderName } from "../../taxes/provider-config";
|
|
||||||
import { TaxJarConfiguration } from "../../taxjar/ui/taxjar-configuration";
|
|
||||||
import { useInstanceId } from "../../taxes/tax-context";
|
|
||||||
import { trpcClient } from "../../trpc/trpc-client";
|
|
||||||
import { AppPaper } from "../../ui/app-paper";
|
|
||||||
import { ProviderIcon } from "./provider-icon";
|
|
||||||
|
|
||||||
const providersConfigurationComponent: Record<TaxProviderName, React.ComponentType> = {
|
|
||||||
taxjar: TaxJarConfiguration,
|
|
||||||
avatax: AvataxConfiguration,
|
|
||||||
};
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
|
||||||
radioLabel: {
|
|
||||||
width: "100%",
|
|
||||||
padding: theme.spacing(1),
|
|
||||||
border: `1px solid ${theme.palette.divider}`,
|
|
||||||
"&:hover": {
|
|
||||||
backgroundColor:
|
|
||||||
theme.palette.type === "dark" ? theme.palette.primary.dark : theme.palette.grey[50],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
gridItem: {
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
},
|
|
||||||
radioLabelActive: {
|
|
||||||
backgroundColor:
|
|
||||||
theme.palette.type === "dark" ? theme.palette.primary.dark : theme.palette.grey[50],
|
|
||||||
},
|
|
||||||
iconWithLabel: {
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: theme.spacing(1),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const Configuration = () => {
|
|
||||||
const [provider, setProvider] = React.useState<TaxProviderName>("taxjar");
|
|
||||||
const { instanceId } = useInstanceId();
|
|
||||||
const { data: providersConfigurationData } = trpcClient.providersConfiguration.getAll.useQuery();
|
|
||||||
const styles = useStyles();
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const instance = providersConfigurationData?.find((instance) => instance.id === instanceId);
|
|
||||||
|
|
||||||
setProvider(instance?.provider ?? "taxjar");
|
|
||||||
}, [instanceId, providersConfigurationData]);
|
|
||||||
|
|
||||||
const SelectedConfigurationForm = React.useMemo(
|
|
||||||
() => (provider ? providersConfigurationComponent[provider] : () => null),
|
|
||||||
[provider]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AppPaper>
|
|
||||||
{!instanceId && (
|
|
||||||
<Grid container spacing={2}>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<div className={styles.gridItem}>
|
|
||||||
<Typography component="h3" variant="h3">
|
|
||||||
Please select one of the providers:
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<RadioGroup
|
|
||||||
value={provider ?? ""}
|
|
||||||
onChange={(e) => setProvider(e.target.value as TaxProviderName)}
|
|
||||||
>
|
|
||||||
<Grid container justifyContent="center">
|
|
||||||
{Object.entries(providerConfig).map(([name, config]) => (
|
|
||||||
<Grid className={styles.gridItem} item xs={6} key={name}>
|
|
||||||
<FormControlLabel
|
|
||||||
className={
|
|
||||||
provider === name
|
|
||||||
? `${styles.radioLabelActive} ${styles.radioLabel}`
|
|
||||||
: styles.radioLabel
|
|
||||||
}
|
|
||||||
control={<Radio style={{ display: "none" }} name="provider" value={name} />}
|
|
||||||
label={
|
|
||||||
<div className={styles.iconWithLabel}>
|
|
||||||
<ProviderIcon size={"xlarge"} provider={name as TaxProviderName} />
|
|
||||||
<Typography variant="body1">{config.label}</Typography>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
labelPlacement="top"
|
|
||||||
aria-label={config.label}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
))}
|
|
||||||
</Grid>
|
|
||||||
</RadioGroup>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
)}
|
|
||||||
<SelectedConfigurationForm />
|
|
||||||
</AppPaper>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,45 +0,0 @@
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogTitle,
|
|
||||||
DialogContent,
|
|
||||||
DialogContentText,
|
|
||||||
DialogActions,
|
|
||||||
} from "@material-ui/core";
|
|
||||||
import { Button, makeStyles } from "@saleor/macaw-ui";
|
|
||||||
|
|
||||||
type DeleteProviderDialogProps = {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
onCancel: () => void;
|
|
||||||
onConfirm: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
|
||||||
actions: {
|
|
||||||
display: "flex",
|
|
||||||
gap: theme.spacing(1),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const DeleteProviderDialog = (p: DeleteProviderDialogProps) => {
|
|
||||||
const styles = useStyles();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={p.isOpen} onClose={p.onClose}>
|
|
||||||
<DialogTitle>Delete provider instance?</DialogTitle>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogContentText>
|
|
||||||
Are you sure you want to delete this provider instance? This action cannot be undone.
|
|
||||||
</DialogContentText>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<div className={styles.actions}>
|
|
||||||
<Button onClick={p.onCancel}>Cancel</Button>
|
|
||||||
<Button variant="primary" onClick={p.onConfirm}>
|
|
||||||
Confirm
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,25 +0,0 @@
|
||||||
import Image, { ImageProps } from "next/image";
|
|
||||||
import { providerConfig, TaxProviderName } from "../../taxes/provider-config";
|
|
||||||
|
|
||||||
type Size = "small" | "medium" | "large" | "xlarge";
|
|
||||||
|
|
||||||
const sizes: Record<Size, number> = {
|
|
||||||
small: 16,
|
|
||||||
medium: 24,
|
|
||||||
large: 32,
|
|
||||||
xlarge: 48,
|
|
||||||
};
|
|
||||||
|
|
||||||
type ProviderIconProps = {
|
|
||||||
provider: TaxProviderName;
|
|
||||||
size?: Size;
|
|
||||||
} & Omit<ImageProps, "src" | "height" | "width" | "alt">;
|
|
||||||
|
|
||||||
export const ProviderIcon = ({ provider, size = "medium", ...props }: ProviderIconProps) => {
|
|
||||||
const { icon, label } = providerConfig[provider];
|
|
||||||
const matchedSize = sizes[size];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Image src={icon} alt={`${label} icon`} width={matchedSize} height={matchedSize} {...props} />
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,70 +0,0 @@
|
||||||
import {
|
|
||||||
makeStyles,
|
|
||||||
OffsettedList,
|
|
||||||
OffsettedListBody,
|
|
||||||
OffsettedListHeader,
|
|
||||||
OffsettedListItem,
|
|
||||||
OffsettedListItemCell,
|
|
||||||
} from "@saleor/macaw-ui";
|
|
||||||
import clsx from "clsx";
|
|
||||||
import { useInstanceId } from "../../taxes/tax-context";
|
|
||||||
import { trpcClient } from "../../trpc/trpc-client";
|
|
||||||
import { AppPaper } from "../../ui/app-paper";
|
|
||||||
import { ProviderIcon } from "./provider-icon";
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => {
|
|
||||||
return {
|
|
||||||
headerItem: {
|
|
||||||
height: "auto !important",
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "1fr",
|
|
||||||
},
|
|
||||||
listItem: {
|
|
||||||
cursor: "pointer",
|
|
||||||
height: "auto !important",
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "1fr",
|
|
||||||
},
|
|
||||||
listItemActive: {
|
|
||||||
border: `2px solid ${theme.palette.primary.main}`,
|
|
||||||
},
|
|
||||||
cell: {
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: theme.spacing(1),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
export const TaxProvidersInstancesList = () => {
|
|
||||||
const styles = useStyles();
|
|
||||||
const { instanceId, setInstanceId } = useInstanceId();
|
|
||||||
const { data: providersConfigurationData } = trpcClient.providersConfiguration.getAll.useQuery();
|
|
||||||
const instances = providersConfigurationData ?? [];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AppPaper>
|
|
||||||
<OffsettedList gridTemplate={["1fr", "1fr"]}>
|
|
||||||
<OffsettedListHeader>
|
|
||||||
<OffsettedListItem className={styles.headerItem}>
|
|
||||||
<OffsettedListItemCell>Tax provider list</OffsettedListItemCell>
|
|
||||||
</OffsettedListItem>
|
|
||||||
</OffsettedListHeader>
|
|
||||||
<OffsettedListBody>
|
|
||||||
{instances.map((instance) => (
|
|
||||||
<OffsettedListItem
|
|
||||||
onClick={() => setInstanceId(instance.id)}
|
|
||||||
className={clsx(styles.listItem, instance.id === instanceId && styles.listItemActive)}
|
|
||||||
key={instance.id}
|
|
||||||
>
|
|
||||||
<OffsettedListItemCell className={styles.cell}>
|
|
||||||
{instance.config.name}
|
|
||||||
<ProviderIcon size="medium" provider={instance.provider} />
|
|
||||||
</OffsettedListItemCell>
|
|
||||||
</OffsettedListItem>
|
|
||||||
))}
|
|
||||||
</OffsettedListBody>
|
|
||||||
</OffsettedList>
|
|
||||||
</AppPaper>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,74 +0,0 @@
|
||||||
import { Grid } from "@material-ui/core";
|
|
||||||
import { Add } from "@material-ui/icons";
|
|
||||||
import { Skeleton } from "@material-ui/lab";
|
|
||||||
import { Button, makeStyles } from "@saleor/macaw-ui";
|
|
||||||
import { useInstanceId } from "../../taxes/tax-context";
|
|
||||||
import { trpcClient } from "../../trpc/trpc-client";
|
|
||||||
import { AppPaper } from "../../ui/app-paper";
|
|
||||||
import { TaxProvidersInstancesList } from "./providers-instances-list";
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => {
|
|
||||||
return {
|
|
||||||
button: {
|
|
||||||
padding: theme.spacing(1, 2),
|
|
||||||
justifyContent: "flex-start",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const ProvidersSkeleton = () => {
|
|
||||||
return (
|
|
||||||
<AppPaper>
|
|
||||||
<Grid container spacing={2}>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Skeleton variant="rect" width={"45%"} height={10} />
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Skeleton variant="rect" width={"100%"} height={30} />
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Skeleton variant="rect" width={"100%"} height={30} />
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</AppPaper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ProvidersInstances = () => {
|
|
||||||
const styles = useStyles();
|
|
||||||
const providers = trpcClient.providersConfiguration.getAll.useQuery();
|
|
||||||
const { setInstanceId } = useInstanceId();
|
|
||||||
|
|
||||||
if (providers?.isFetching) {
|
|
||||||
return <ProvidersSkeleton />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (providers.error) {
|
|
||||||
return <div>Error. No provider available</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isAnyProvider = providers.data?.length !== 0;
|
|
||||||
|
|
||||||
if (!isAnyProvider) {
|
|
||||||
return <div></div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Grid container spacing={2}>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<TaxProvidersInstancesList />
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
startIcon={<Add />}
|
|
||||||
className={styles.button}
|
|
||||||
fullWidth
|
|
||||||
onClick={() => setInstanceId(null)}
|
|
||||||
>
|
|
||||||
Add provider
|
|
||||||
</Button>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -24,9 +24,18 @@ const mockedProviders: ProvidersConfig = [
|
||||||
isAutocommit: false,
|
isAutocommit: false,
|
||||||
isSandbox: true,
|
isSandbox: true,
|
||||||
name: "avatax-1",
|
name: "avatax-1",
|
||||||
password: "avatax-password",
|
|
||||||
username: "avatax-username",
|
|
||||||
shippingTaxCode: "FR000000",
|
shippingTaxCode: "FR000000",
|
||||||
|
credentials: {
|
||||||
|
password: "avatax-password",
|
||||||
|
username: "avatax-username",
|
||||||
|
},
|
||||||
|
address: {
|
||||||
|
city: "New York",
|
||||||
|
country: "US",
|
||||||
|
state: "NY",
|
||||||
|
street: "123 Main St",
|
||||||
|
zip: "10001",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -34,8 +43,17 @@ const mockedProviders: ProvidersConfig = [
|
||||||
id: "2",
|
id: "2",
|
||||||
config: {
|
config: {
|
||||||
name: "taxjar-1",
|
name: "taxjar-1",
|
||||||
apiKey: "taxjar-api-key",
|
|
||||||
isSandbox: true,
|
isSandbox: true,
|
||||||
|
credentials: {
|
||||||
|
apiKey: "taxjar-api-key",
|
||||||
|
},
|
||||||
|
address: {
|
||||||
|
city: "New York",
|
||||||
|
country: "US",
|
||||||
|
state: "NY",
|
||||||
|
street: "123 Main St",
|
||||||
|
zip: "10001",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -43,28 +61,12 @@ const mockedEncryptedProviders = encrypt(JSON.stringify(mockedProviders), mocked
|
||||||
|
|
||||||
const mockedChannelsWithInvalidProviderInstanceId: ChannelsConfig = {
|
const mockedChannelsWithInvalidProviderInstanceId: ChannelsConfig = {
|
||||||
"default-channel": {
|
"default-channel": {
|
||||||
address: {
|
|
||||||
city: "New York",
|
|
||||||
country: "US",
|
|
||||||
state: "NY",
|
|
||||||
street: "123 Main St",
|
|
||||||
zip: "10001",
|
|
||||||
},
|
|
||||||
enabled: true,
|
|
||||||
providerInstanceId: "3",
|
providerInstanceId: "3",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockedValidChannels: ChannelsConfig = {
|
const mockedValidChannels: ChannelsConfig = {
|
||||||
"default-channel": {
|
"default-channel": {
|
||||||
address: {
|
|
||||||
city: "New York",
|
|
||||||
country: "US",
|
|
||||||
state: "NY",
|
|
||||||
street: "123 Main St",
|
|
||||||
zip: "10001",
|
|
||||||
},
|
|
||||||
enabled: true,
|
|
||||||
providerInstanceId: "1",
|
providerInstanceId: "1",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -8,18 +8,16 @@ import { Logger, createLogger } from "../../lib/logger";
|
||||||
|
|
||||||
import { getAppConfig } from "../app/get-app-config";
|
import { getAppConfig } from "../app/get-app-config";
|
||||||
import { AvataxWebhookService } from "../avatax/avatax-webhook.service";
|
import { AvataxWebhookService } from "../avatax/avatax-webhook.service";
|
||||||
import { ChannelConfig } from "../channels-configuration/channels-config";
|
|
||||||
import { ProviderConfig } from "../providers-configuration/providers-config";
|
import { ProviderConfig } from "../providers-configuration/providers-config";
|
||||||
import { TaxJarWebhookService } from "../taxjar/taxjar-webhook.service";
|
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 {
|
export class ActiveTaxProvider implements ProviderWebhookService {
|
||||||
private client: ProviderWebhookService;
|
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
private channel: ChannelConfig;
|
private client: TaxJarWebhookService | AvataxWebhookService;
|
||||||
|
|
||||||
constructor(providerInstance: ProviderConfig, channelConfig: ChannelConfig) {
|
constructor(providerInstance: ProviderConfig) {
|
||||||
this.logger = createLogger({
|
this.logger = createLogger({
|
||||||
service: "ActiveTaxProvider",
|
service: "ActiveTaxProvider",
|
||||||
});
|
});
|
||||||
|
@ -27,7 +25,6 @@ export class ActiveTaxProvider implements ProviderWebhookService {
|
||||||
const taxProviderName = providerInstance.provider;
|
const taxProviderName = providerInstance.provider;
|
||||||
|
|
||||||
this.logger.trace({ taxProviderName }, "Constructing tax provider: ");
|
this.logger.trace({ taxProviderName }, "Constructing tax provider: ");
|
||||||
this.channel = channelConfig;
|
|
||||||
|
|
||||||
switch (taxProviderName) {
|
switch (taxProviderName) {
|
||||||
case "taxjar":
|
case "taxjar":
|
||||||
|
@ -47,19 +44,19 @@ export class ActiveTaxProvider implements ProviderWebhookService {
|
||||||
async calculateTaxes(payload: TaxBaseFragment) {
|
async calculateTaxes(payload: TaxBaseFragment) {
|
||||||
this.logger.trace({ payload }, ".calculate called");
|
this.logger.trace({ payload }, ".calculate called");
|
||||||
|
|
||||||
return this.client.calculateTaxes(payload, this.channel);
|
return this.client.calculateTaxes(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOrder(order: OrderCreatedSubscriptionFragment) {
|
async createOrder(order: OrderCreatedSubscriptionFragment) {
|
||||||
this.logger.trace(".createOrder called");
|
this.logger.trace(".createOrder called");
|
||||||
|
|
||||||
return this.client.createOrder(order, this.channel);
|
return this.client.createOrder(order);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fulfillOrder(payload: OrderFulfilledSubscriptionFragment) {
|
async fulfillOrder(payload: OrderFulfilledSubscriptionFragment) {
|
||||||
this.logger.trace(".fulfillOrder called");
|
this.logger.trace(".fulfillOrder called");
|
||||||
|
|
||||||
return this.client.fulfillOrder(payload, this.channel);
|
return this.client.fulfillOrder(payload);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,7 +89,7 @@ export function getActiveTaxProvider(
|
||||||
throw new Error(`Channel (${channelSlug}) providerInstanceId does not match any providers`);
|
throw new Error(`Channel (${channelSlug}) providerInstanceId does not match any providers`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const taxProvider = new ActiveTaxProvider(providerInstance, channelConfig);
|
const taxProvider = new ActiveTaxProvider(providerInstance);
|
||||||
|
|
||||||
return taxProvider;
|
return taxProvider;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
import { AvataxIcon, TaxJarIcon } from "../../assets";
|
|
||||||
|
|
||||||
export const providerConfig = {
|
|
||||||
taxjar: {
|
|
||||||
label: "TaxJar",
|
|
||||||
icon: TaxJarIcon,
|
|
||||||
},
|
|
||||||
avatax: {
|
|
||||||
label: "Avatax",
|
|
||||||
icon: AvataxIcon,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TaxProviderName = keyof typeof providerConfig;
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { atom, useAtom } from "jotai";
|
import { atom, useAtom } from "jotai";
|
||||||
import { AppTab } from "../../pages/configuration";
|
|
||||||
|
|
||||||
const channelSlugAtom = atom("");
|
const channelSlugAtom = atom("");
|
||||||
|
|
||||||
|
@ -16,11 +15,3 @@ export const useInstanceId = () => {
|
||||||
|
|
||||||
return { instanceId, setInstanceId };
|
return { instanceId, setInstanceId };
|
||||||
};
|
};
|
||||||
|
|
||||||
const activeTabAtom = atom<AppTab>("channels");
|
|
||||||
|
|
||||||
export const useActiveTab = () => {
|
|
||||||
const [activeTab, setActiveTab] = useAtom(activeTabAtom);
|
|
||||||
|
|
||||||
return { activeTab, setActiveTab };
|
|
||||||
};
|
|
||||||
|
|
|
@ -11,16 +11,7 @@ export type CalculateTaxesResponse = SyncWebhookResponsesMap["ORDER_CALCULATE_TA
|
||||||
export type CreateOrderResponse = { id: string };
|
export type CreateOrderResponse = { id: string };
|
||||||
|
|
||||||
export interface ProviderWebhookService {
|
export interface ProviderWebhookService {
|
||||||
calculateTaxes: (
|
calculateTaxes: (payload: TaxBaseFragment) => Promise<CalculateTaxesResponse>;
|
||||||
payload: TaxBaseFragment,
|
createOrder: (payload: OrderCreatedSubscriptionFragment) => Promise<CreateOrderResponse>;
|
||||||
channel: ChannelConfig
|
fulfillOrder: (payload: OrderFulfilledSubscriptionFragment) => Promise<{ ok: boolean }>;
|
||||||
) => Promise<CalculateTaxesResponse>;
|
|
||||||
createOrder: (
|
|
||||||
payload: OrderCreatedSubscriptionFragment,
|
|
||||||
channel: ChannelConfig
|
|
||||||
) => Promise<CreateOrderResponse>;
|
|
||||||
fulfillOrder: (
|
|
||||||
payload: OrderFulfilledSubscriptionFragment,
|
|
||||||
channel: ChannelConfig
|
|
||||||
) => Promise<{ ok: boolean }>;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { ChannelAddress } from "../channels-configuration/channels-config";
|
import { TaxParams } from "taxjar/dist/types/paramTypes";
|
||||||
import { AddressFragment as SaleorAddress } from "../../../generated/graphql";
|
import { AddressFragment as SaleorAddress } from "../../../generated/graphql";
|
||||||
import { AddressParams as TaxJarAddress, TaxParams } from "taxjar/dist/types/paramTypes";
|
import { TaxJarConfig } from "./taxjar-config";
|
||||||
|
|
||||||
function joinAddresses(address1: string, address2: string): string {
|
function joinAddresses(address1: string, address2: string): string {
|
||||||
return `${address1}${address2.length > 0 ? " " + address2 : ""}`;
|
return `${address1}${address2.length > 0 ? " " + address2 : ""}`;
|
||||||
|
@ -19,7 +19,7 @@ function mapSaleorAddressToTaxJarAddress(
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapChannelAddressToTaxJarAddress(
|
function mapChannelAddressToTaxJarAddress(
|
||||||
address: ChannelAddress
|
address: TaxJarConfig["address"]
|
||||||
): Pick<TaxParams, "from_city" | "from_country" | "from_state" | "from_street" | "from_zip"> {
|
): Pick<TaxParams, "from_city" | "from_country" | "from_state" | "from_street" | "from_zip"> {
|
||||||
return {
|
return {
|
||||||
from_city: address.city,
|
from_city: address.city,
|
||||||
|
|
|
@ -8,23 +8,24 @@ import { TaxJarCalculateTaxesPayloadTransformer } from "./taxjar-calculate-taxes
|
||||||
import { TaxJarCalculateTaxesResponseTransformer } from "./taxjar-calculate-taxes-response-transformer";
|
import { TaxJarCalculateTaxesResponseTransformer } from "./taxjar-calculate-taxes-response-transformer";
|
||||||
import { Logger, createLogger } from "../../../lib/logger";
|
import { Logger, createLogger } from "../../../lib/logger";
|
||||||
|
|
||||||
export type Payload = {
|
export type TaxJarCalculateTaxesPayload = {
|
||||||
taxBase: TaxBaseFragment;
|
taxBase: TaxBaseFragment;
|
||||||
channelConfig: ChannelConfig;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Target = FetchTaxForOrderArgs;
|
export type TaxJarCalculateTaxesTarget = FetchTaxForOrderArgs;
|
||||||
export type Response = CalculateTaxesResponse;
|
export type TaxJarCalculateTaxesResponse = CalculateTaxesResponse;
|
||||||
|
|
||||||
export class TaxJarCalculateTaxesAdapter implements WebhookAdapter<Payload, Response> {
|
export class TaxJarCalculateTaxesAdapter
|
||||||
|
implements WebhookAdapter<TaxJarCalculateTaxesPayload, TaxJarCalculateTaxesResponse>
|
||||||
|
{
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
constructor(private readonly config: TaxJarConfig) {
|
constructor(private readonly config: TaxJarConfig) {
|
||||||
this.logger = createLogger({ service: "TaxJarCalculateTaxesAdapter" });
|
this.logger = createLogger({ service: "TaxJarCalculateTaxesAdapter" });
|
||||||
}
|
}
|
||||||
|
|
||||||
async send(payload: Payload): Promise<Response> {
|
async send(payload: TaxJarCalculateTaxesPayload): Promise<TaxJarCalculateTaxesResponse> {
|
||||||
this.logger.debug({ payload }, "send called with:");
|
this.logger.debug({ payload }, "send called with:");
|
||||||
const payloadTransformer = new TaxJarCalculateTaxesPayloadTransformer();
|
const payloadTransformer = new TaxJarCalculateTaxesPayloadTransformer(this.config);
|
||||||
const target = payloadTransformer.transform(payload);
|
const target = payloadTransformer.transform(payload);
|
||||||
|
|
||||||
this.logger.debug({ transformedPayload: target }, "Will call fetchTaxForOrder with:");
|
this.logger.debug({ transformedPayload: target }, "Will call fetchTaxForOrder with:");
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { TaxForOrderRes } from "taxjar/dist/types/returnTypes";
|
import { TaxForOrderRes } from "taxjar/dist/types/returnTypes";
|
||||||
import { TaxBaseFragment } from "../../../../generated/graphql";
|
import { TaxBaseFragment } from "../../../../generated/graphql";
|
||||||
import { ChannelConfig } from "../../channels-configuration/channels-config";
|
import { ChannelConfig } from "../../channels-configuration/channels-config";
|
||||||
|
import { TaxJarConfig } from "../taxjar-config";
|
||||||
|
|
||||||
type TaxBase = TaxBaseFragment;
|
type TaxBase = TaxBaseFragment;
|
||||||
|
|
||||||
|
@ -194,21 +195,16 @@ const taxExcludedTaxBase: TaxBase = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const withNexusChannelConfig: ChannelConfig = {
|
const channelConfig: ChannelConfig = {
|
||||||
providerInstanceId: "b8c29f49-7cae-4762-8458-e9a27eb83081",
|
providerInstanceId: "b8c29f49-7cae-4762-8458-e9a27eb83081",
|
||||||
enabled: false,
|
|
||||||
address: {
|
|
||||||
country: "US",
|
|
||||||
zip: "10118",
|
|
||||||
state: "NY",
|
|
||||||
city: "New York",
|
|
||||||
street: "350 5th Avenue",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const noNexusChannelConfig: ChannelConfig = {
|
const providerConfig: TaxJarConfig = {
|
||||||
providerInstanceId: "aa5293e5-7f5d-4782-a619-222ead918e50",
|
name: "taxjar-1",
|
||||||
enabled: false,
|
isSandbox: false,
|
||||||
|
credentials: {
|
||||||
|
apiKey: "test",
|
||||||
|
},
|
||||||
address: {
|
address: {
|
||||||
country: "US",
|
country: "US",
|
||||||
zip: "10118",
|
zip: "10118",
|
||||||
|
@ -461,22 +457,26 @@ const withNexusTaxIncludedTaxForOrderMock: TaxForOrder = {
|
||||||
const testingScenariosMap = {
|
const testingScenariosMap = {
|
||||||
with_no_nexus_tax_included: {
|
with_no_nexus_tax_included: {
|
||||||
taxBase: taxIncludedTaxBase,
|
taxBase: taxIncludedTaxBase,
|
||||||
channelConfig: noNexusChannelConfig,
|
channelConfig,
|
||||||
|
providerConfig,
|
||||||
response: noNexusTaxForOrderMock,
|
response: noNexusTaxForOrderMock,
|
||||||
},
|
},
|
||||||
with_no_nexus_tax_excluded: {
|
with_no_nexus_tax_excluded: {
|
||||||
taxBase: taxExcludedTaxBase,
|
taxBase: taxExcludedTaxBase,
|
||||||
channelConfig: noNexusChannelConfig,
|
channelConfig,
|
||||||
|
providerConfig,
|
||||||
response: noNexusTaxForOrderMock,
|
response: noNexusTaxForOrderMock,
|
||||||
},
|
},
|
||||||
with_nexus_tax_included: {
|
with_nexus_tax_included: {
|
||||||
taxBase: taxIncludedTaxBase,
|
taxBase: taxIncludedTaxBase,
|
||||||
channelConfig: withNexusChannelConfig,
|
channelConfig,
|
||||||
|
providerConfig,
|
||||||
response: withNexusTaxIncludedTaxForOrderMock,
|
response: withNexusTaxIncludedTaxForOrderMock,
|
||||||
},
|
},
|
||||||
with_nexus_tax_excluded: {
|
with_nexus_tax_excluded: {
|
||||||
taxBase: taxExcludedTaxBase,
|
taxBase: taxExcludedTaxBase,
|
||||||
channelConfig: withNexusChannelConfig,
|
channelConfig,
|
||||||
|
providerConfig,
|
||||||
response: withNexusTaxExcludedTaxForOrderMock,
|
response: withNexusTaxExcludedTaxForOrderMock,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -497,6 +497,12 @@ export class TaxJarCalculateTaxesMockGenerator {
|
||||||
...overrides,
|
...overrides,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
generateProviderConfig = (overrides: Partial<TaxJarConfig> = {}): TaxJarConfig =>
|
||||||
|
structuredClone({
|
||||||
|
...testingScenariosMap[this.scenario].providerConfig,
|
||||||
|
...overrides,
|
||||||
|
});
|
||||||
|
|
||||||
generateResponse = (overrides: Partial<TaxForOrder> = {}): TaxForOrder =>
|
generateResponse = (overrides: Partial<TaxForOrder> = {}): TaxForOrder =>
|
||||||
structuredClone({
|
structuredClone({
|
||||||
...testingScenariosMap[this.scenario].response,
|
...testingScenariosMap[this.scenario].response,
|
||||||
|
|
|
@ -2,16 +2,15 @@ import { describe, expect, it } from "vitest";
|
||||||
import { TaxJarCalculateTaxesMockGenerator } from "./taxjar-calculate-taxes-mock-generator";
|
import { TaxJarCalculateTaxesMockGenerator } from "./taxjar-calculate-taxes-mock-generator";
|
||||||
import { TaxJarCalculateTaxesPayloadTransformer } from "./taxjar-calculate-taxes-payload-transformer";
|
import { TaxJarCalculateTaxesPayloadTransformer } from "./taxjar-calculate-taxes-payload-transformer";
|
||||||
|
|
||||||
const transformer = new TaxJarCalculateTaxesPayloadTransformer();
|
|
||||||
|
|
||||||
describe("TaxJarCalculateTaxesPayloadTransformer", () => {
|
describe("TaxJarCalculateTaxesPayloadTransformer", () => {
|
||||||
|
const mockGenerator = new TaxJarCalculateTaxesMockGenerator("with_nexus_tax_included");
|
||||||
|
const providerConfig = mockGenerator.generateProviderConfig();
|
||||||
|
const transformer = new TaxJarCalculateTaxesPayloadTransformer(providerConfig);
|
||||||
|
|
||||||
it("returns payload containing line_items without discounts", () => {
|
it("returns payload containing line_items without discounts", () => {
|
||||||
const mockGenerator = new TaxJarCalculateTaxesMockGenerator("with_nexus_tax_included");
|
|
||||||
const taxBase = mockGenerator.generateTaxBase();
|
const taxBase = mockGenerator.generateTaxBase();
|
||||||
const channelConfig = mockGenerator.generateChannelConfig();
|
|
||||||
const transformedPayload = transformer.transform({
|
const transformedPayload = transformer.transform({
|
||||||
taxBase,
|
taxBase,
|
||||||
channelConfig,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(transformedPayload).toEqual({
|
expect(transformedPayload).toEqual({
|
||||||
|
@ -62,10 +61,8 @@ describe("TaxJarCalculateTaxesPayloadTransformer", () => {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
const channelConfig = mockGenerator.generateChannelConfig();
|
|
||||||
const transformedPayload = transformer.transform({
|
const transformedPayload = transformer.transform({
|
||||||
taxBase,
|
taxBase,
|
||||||
channelConfig,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const payloadLines = transformedPayload.params.line_items ?? [];
|
const payloadLines = transformedPayload.params.line_items ?? [];
|
||||||
|
@ -100,12 +97,10 @@ describe("TaxJarCalculateTaxesPayloadTransformer", () => {
|
||||||
it("throws error when no address", () => {
|
it("throws error when no address", () => {
|
||||||
const mockGenerator = new TaxJarCalculateTaxesMockGenerator("with_nexus_tax_included");
|
const mockGenerator = new TaxJarCalculateTaxesMockGenerator("with_nexus_tax_included");
|
||||||
const taxBase = mockGenerator.generateTaxBase({ address: null });
|
const taxBase = mockGenerator.generateTaxBase({ address: null });
|
||||||
const channelConfig = mockGenerator.generateChannelConfig();
|
|
||||||
|
|
||||||
expect(() =>
|
expect(() =>
|
||||||
transformer.transform({
|
transformer.transform({
|
||||||
taxBase,
|
taxBase,
|
||||||
channelConfig,
|
|
||||||
})
|
})
|
||||||
).toThrow("Customer address is required to calculate taxes in TaxJar.");
|
).toThrow("Customer address is required to calculate taxes in TaxJar.");
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,9 +1,16 @@
|
||||||
import { discountUtils } from "../../taxes/discount-utils";
|
import { discountUtils } from "../../taxes/discount-utils";
|
||||||
import { taxJarAddressFactory } from "../address-factory";
|
import { taxJarAddressFactory } from "../address-factory";
|
||||||
import { Payload, Target } from "./taxjar-calculate-taxes-adapter";
|
import { TaxJarConfig } from "../taxjar-config";
|
||||||
|
import {
|
||||||
|
TaxJarCalculateTaxesPayload,
|
||||||
|
TaxJarCalculateTaxesTarget,
|
||||||
|
} from "./taxjar-calculate-taxes-adapter";
|
||||||
|
|
||||||
export class TaxJarCalculateTaxesPayloadTransformer {
|
export class TaxJarCalculateTaxesPayloadTransformer {
|
||||||
private mapLines(taxBase: Payload["taxBase"]): Target["params"]["line_items"] {
|
constructor(private readonly config: TaxJarConfig) {}
|
||||||
|
private mapLines(
|
||||||
|
taxBase: TaxJarCalculateTaxesPayload["taxBase"]
|
||||||
|
): TaxJarCalculateTaxesTarget["params"]["line_items"] {
|
||||||
const { lines, discounts } = taxBase;
|
const { lines, discounts } = taxBase;
|
||||||
const discountSum = discounts?.reduce(
|
const discountSum = discounts?.reduce(
|
||||||
(total, current) => total + Number(current.amount.amount),
|
(total, current) => total + Number(current.amount.amount),
|
||||||
|
@ -12,24 +19,26 @@ export class TaxJarCalculateTaxesPayloadTransformer {
|
||||||
const linePrices = lines.map((line) => Number(line.totalPrice.amount));
|
const linePrices = lines.map((line) => Number(line.totalPrice.amount));
|
||||||
const distributedDiscounts = discountUtils.distributeDiscount(discountSum, linePrices);
|
const distributedDiscounts = discountUtils.distributeDiscount(discountSum, linePrices);
|
||||||
|
|
||||||
const mappedLines: Target["params"]["line_items"] = lines.map((line, index) => {
|
const mappedLines: TaxJarCalculateTaxesTarget["params"]["line_items"] = lines.map(
|
||||||
const discountAmount = distributedDiscounts[index];
|
(line, index) => {
|
||||||
|
const discountAmount = distributedDiscounts[index];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: line.sourceLine.id,
|
id: line.sourceLine.id,
|
||||||
// todo: get from tax code matcher
|
// todo: get from tax code matcher
|
||||||
product_tax_code: "",
|
product_tax_code: "",
|
||||||
quantity: line.quantity,
|
quantity: line.quantity,
|
||||||
unit_price: Number(line.unitPrice.amount),
|
unit_price: Number(line.unitPrice.amount),
|
||||||
discount: discountAmount,
|
discount: discountAmount,
|
||||||
};
|
};
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return mappedLines;
|
return mappedLines;
|
||||||
}
|
}
|
||||||
|
|
||||||
transform({ taxBase, channelConfig }: Payload): Target {
|
transform({ taxBase }: TaxJarCalculateTaxesPayload): TaxJarCalculateTaxesTarget {
|
||||||
const fromAddress = taxJarAddressFactory.fromChannelAddress(channelConfig.address);
|
const fromAddress = taxJarAddressFactory.fromChannelAddress(this.config.address);
|
||||||
|
|
||||||
if (!taxBase.address) {
|
if (!taxBase.address) {
|
||||||
throw new Error("Customer address is required to calculate taxes in TaxJar.");
|
throw new Error("Customer address is required to calculate taxes in TaxJar.");
|
||||||
|
@ -37,7 +46,7 @@ export class TaxJarCalculateTaxesPayloadTransformer {
|
||||||
|
|
||||||
const toAddress = taxJarAddressFactory.fromSaleorAddress(taxBase.address);
|
const toAddress = taxJarAddressFactory.fromSaleorAddress(taxBase.address);
|
||||||
|
|
||||||
const taxParams: Target = {
|
const taxParams: TaxJarCalculateTaxesTarget = {
|
||||||
params: {
|
params: {
|
||||||
...fromAddress,
|
...fromAddress,
|
||||||
...toAddress,
|
...toAddress,
|
||||||
|
|
|
@ -2,7 +2,10 @@ import Breakdown from "taxjar/dist/types/breakdown";
|
||||||
import { TaxForOrderRes } from "taxjar/dist/types/returnTypes";
|
import { TaxForOrderRes } from "taxjar/dist/types/returnTypes";
|
||||||
import { TaxBaseFragment } from "../../../../generated/graphql";
|
import { TaxBaseFragment } from "../../../../generated/graphql";
|
||||||
import { taxProviderUtils } from "../../taxes/tax-provider-utils";
|
import { taxProviderUtils } from "../../taxes/tax-provider-utils";
|
||||||
import { Payload, Response } from "./taxjar-calculate-taxes-adapter";
|
import {
|
||||||
|
TaxJarCalculateTaxesPayload,
|
||||||
|
TaxJarCalculateTaxesResponse,
|
||||||
|
} from "./taxjar-calculate-taxes-adapter";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* TaxJar doesn't guarantee the order of the response items to match the payload items order.
|
* TaxJar doesn't guarantee the order of the response items to match the payload items order.
|
||||||
|
@ -26,7 +29,10 @@ export function matchPayloadLinesToResponseLines(
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TaxJarCalculateTaxesResponseLinesTransformer {
|
export class TaxJarCalculateTaxesResponseLinesTransformer {
|
||||||
transform(payload: Payload, response: TaxForOrderRes): Response["lines"] {
|
transform(
|
||||||
|
payload: TaxJarCalculateTaxesPayload,
|
||||||
|
response: TaxForOrderRes
|
||||||
|
): TaxJarCalculateTaxesResponse["lines"] {
|
||||||
const responseLines = response.tax.breakdown?.line_items ?? [];
|
const responseLines = response.tax.breakdown?.line_items ?? [];
|
||||||
|
|
||||||
const lines = matchPayloadLinesToResponseLines(payload.taxBase.lines, responseLines);
|
const lines = matchPayloadLinesToResponseLines(payload.taxBase.lines, responseLines);
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
import { TaxForOrderRes } from "taxjar/dist/types/returnTypes";
|
import { TaxForOrderRes } from "taxjar/dist/types/returnTypes";
|
||||||
import { numbers } from "../../taxes/numbers";
|
import { numbers } from "../../taxes/numbers";
|
||||||
import { Payload, Response } from "./taxjar-calculate-taxes-adapter";
|
import {
|
||||||
|
TaxJarCalculateTaxesResponse,
|
||||||
|
TaxJarCalculateTaxesPayload,
|
||||||
|
} from "./taxjar-calculate-taxes-adapter";
|
||||||
|
|
||||||
export class TaxJarCalculateTaxesResponseShippingTransformer {
|
export class TaxJarCalculateTaxesResponseShippingTransformer {
|
||||||
transform(
|
transform(
|
||||||
taxBase: Payload["taxBase"],
|
taxBase: TaxJarCalculateTaxesPayload["taxBase"],
|
||||||
res: TaxForOrderRes
|
res: TaxForOrderRes
|
||||||
): Pick<
|
): Pick<
|
||||||
Response,
|
TaxJarCalculateTaxesResponse,
|
||||||
"shipping_price_gross_amount" | "shipping_price_net_amount" | "shipping_tax_rate"
|
"shipping_price_gross_amount" | "shipping_price_net_amount" | "shipping_tax_rate"
|
||||||
> {
|
> {
|
||||||
const { tax } = res;
|
const { tax } = res;
|
||||||
|
|
|
@ -10,7 +10,7 @@ describe("TaxJarCalculateTaxesResponseTransformer", () => {
|
||||||
const noNexusResponseMock = mockGenerator.generateResponse();
|
const noNexusResponseMock = mockGenerator.generateResponse();
|
||||||
const payloadMock = {
|
const payloadMock = {
|
||||||
taxBase: mockGenerator.generateTaxBase(),
|
taxBase: mockGenerator.generateTaxBase(),
|
||||||
channelConfig: mockGenerator.generateChannelConfig(),
|
providerConfig: mockGenerator.generateProviderConfig(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = transformer.transform(payloadMock, noNexusResponseMock);
|
const result = transformer.transform(payloadMock, noNexusResponseMock);
|
||||||
|
@ -44,7 +44,7 @@ describe("TaxJarCalculateTaxesResponseTransformer", () => {
|
||||||
|
|
||||||
const payloadMock = {
|
const payloadMock = {
|
||||||
taxBase: mockGenerator.generateTaxBase(),
|
taxBase: mockGenerator.generateTaxBase(),
|
||||||
channelConfig: mockGenerator.generateChannelConfig(),
|
providerConfig: mockGenerator.generateProviderConfig(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = transformer.transform(payloadMock, nexusResponse);
|
const result = transformer.transform(payloadMock, nexusResponse);
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
import { TaxForOrderRes } from "taxjar/dist/types/returnTypes";
|
import { TaxForOrderRes } from "taxjar/dist/types/returnTypes";
|
||||||
import { Logger, createLogger } from "../../../lib/logger";
|
import { Logger, createLogger } from "../../../lib/logger";
|
||||||
import { Payload, Response } from "./taxjar-calculate-taxes-adapter";
|
import {
|
||||||
|
TaxJarCalculateTaxesResponse,
|
||||||
|
TaxJarCalculateTaxesPayload,
|
||||||
|
} from "./taxjar-calculate-taxes-adapter";
|
||||||
import { TaxJarCalculateTaxesResponseLinesTransformer } from "./taxjar-calculate-taxes-response-lines-transformer";
|
import { TaxJarCalculateTaxesResponseLinesTransformer } from "./taxjar-calculate-taxes-response-lines-transformer";
|
||||||
import { TaxJarCalculateTaxesResponseShippingTransformer } from "./taxjar-calculate-taxes-response-shipping-transformer";
|
import { TaxJarCalculateTaxesResponseShippingTransformer } from "./taxjar-calculate-taxes-response-shipping-transformer";
|
||||||
|
|
||||||
|
@ -11,7 +14,10 @@ export class TaxJarCalculateTaxesResponseTransformer {
|
||||||
this.logger = createLogger({ name: "TaxJarCalculateTaxesResponseTransformer" });
|
this.logger = createLogger({ name: "TaxJarCalculateTaxesResponseTransformer" });
|
||||||
}
|
}
|
||||||
|
|
||||||
transform(payload: Payload, response: TaxForOrderRes): Response {
|
transform(
|
||||||
|
payload: TaxJarCalculateTaxesPayload,
|
||||||
|
response: TaxForOrderRes
|
||||||
|
): TaxJarCalculateTaxesResponse {
|
||||||
/*
|
/*
|
||||||
* TaxJar operates on the idea of sales tax nexus. Nexus is a place where the company has a physical presence.
|
* TaxJar operates on the idea of sales tax nexus. Nexus is a place where the company has a physical presence.
|
||||||
* If the company has no nexus in the state where the customer is located, the company is not required to collect sales tax.
|
* If the company has no nexus in the state where the customer is located, the company is not required to collect sales tax.
|
||||||
|
|
|
@ -1,27 +1,31 @@
|
||||||
|
import { CreateOrderRes } from "taxjar/dist/types/returnTypes";
|
||||||
import { OrderCreatedSubscriptionFragment } from "../../../../generated/graphql";
|
import { OrderCreatedSubscriptionFragment } from "../../../../generated/graphql";
|
||||||
import { ChannelConfig } from "../../channels-configuration/channels-config";
|
import { Logger, createLogger } from "../../../lib/logger";
|
||||||
import { CreateOrderResponse } from "../../taxes/tax-provider-webhook";
|
import { CreateOrderResponse } from "../../taxes/tax-provider-webhook";
|
||||||
import { WebhookAdapter } from "../../taxes/tax-webhook-adapter";
|
import { WebhookAdapter } from "../../taxes/tax-webhook-adapter";
|
||||||
import { TaxJarOrderCreatedPayloadTransformer } from "./taxjar-order-created-payload-transformer";
|
|
||||||
import { CreateOrderArgs, TaxJarClient } from "../taxjar-client";
|
import { CreateOrderArgs, TaxJarClient } from "../taxjar-client";
|
||||||
import { TaxJarConfig } from "../taxjar-config";
|
import { TaxJarConfig } from "../taxjar-config";
|
||||||
|
import { TaxJarOrderCreatedPayloadTransformer } from "./taxjar-order-created-payload-transformer";
|
||||||
import { TaxJarOrderCreatedResponseTransformer } from "./taxjar-order-created-response-transformer";
|
import { TaxJarOrderCreatedResponseTransformer } from "./taxjar-order-created-response-transformer";
|
||||||
import { Logger, createLogger } from "../../../lib/logger";
|
|
||||||
|
|
||||||
export type Payload = { order: OrderCreatedSubscriptionFragment; channelConfig: ChannelConfig };
|
export type TaxJarOrderCreatedPayload = {
|
||||||
export type Target = CreateOrderArgs;
|
order: OrderCreatedSubscriptionFragment;
|
||||||
type Response = CreateOrderResponse;
|
};
|
||||||
|
export type TaxJarOrderCreatedTarget = CreateOrderArgs;
|
||||||
|
export type TaxJarOrderCreatedResponse = CreateOrderResponse;
|
||||||
|
|
||||||
export class TaxJarOrderCreatedAdapter implements WebhookAdapter<Payload, Response> {
|
export class TaxJarOrderCreatedAdapter
|
||||||
|
implements WebhookAdapter<TaxJarOrderCreatedPayload, TaxJarOrderCreatedResponse>
|
||||||
|
{
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
constructor(private readonly config: TaxJarConfig) {
|
constructor(private readonly config: TaxJarConfig) {
|
||||||
this.logger = createLogger({ service: "TaxJarOrderCreatedAdapter" });
|
this.logger = createLogger({ service: "TaxJarOrderCreatedAdapter" });
|
||||||
}
|
}
|
||||||
|
|
||||||
async send(payload: Payload): Promise<Response> {
|
async send(payload: TaxJarOrderCreatedPayload): Promise<TaxJarOrderCreatedResponse> {
|
||||||
this.logger.debug({ payload }, "send called with:");
|
this.logger.debug({ payload }, "send called with:");
|
||||||
|
|
||||||
const payloadTransformer = new TaxJarOrderCreatedPayloadTransformer();
|
const payloadTransformer = new TaxJarOrderCreatedPayloadTransformer(this.config);
|
||||||
const target = payloadTransformer.transform(payload);
|
const target = payloadTransformer.transform(payload);
|
||||||
|
|
||||||
const client = new TaxJarClient(this.config);
|
const client = new TaxJarClient(this.config);
|
||||||
|
|
|
@ -2,12 +2,21 @@ import { CreateOrderRes } from "taxjar/dist/types/returnTypes";
|
||||||
import { OrderCreatedSubscriptionFragment, OrderStatus } from "../../../../generated/graphql";
|
import { OrderCreatedSubscriptionFragment, OrderStatus } from "../../../../generated/graphql";
|
||||||
import { ChannelConfig } from "../../channels-configuration/channels-config";
|
import { ChannelConfig } from "../../channels-configuration/channels-config";
|
||||||
import { defaultOrder } from "../../../mocks";
|
import { defaultOrder } from "../../../mocks";
|
||||||
|
import { TaxJarConfig } from "../taxjar-config";
|
||||||
|
|
||||||
type Order = OrderCreatedSubscriptionFragment;
|
type Order = OrderCreatedSubscriptionFragment;
|
||||||
|
|
||||||
const defaultChannelConfig: ChannelConfig = {
|
const defaultChannelConfig: ChannelConfig = {
|
||||||
providerInstanceId: "aa5293e5-7f5d-4782-a619-222ead918e50",
|
providerInstanceId: "aa5293e5-7f5d-4782-a619-222ead918e50",
|
||||||
enabled: false,
|
};
|
||||||
|
|
||||||
|
// providerConfigMockGenerator class that other classes extend?
|
||||||
|
const defaultProviderConfig: TaxJarConfig = {
|
||||||
|
name: "taxjar-1",
|
||||||
|
credentials: {
|
||||||
|
apiKey: "test",
|
||||||
|
},
|
||||||
|
isSandbox: false,
|
||||||
address: {
|
address: {
|
||||||
country: "US",
|
country: "US",
|
||||||
zip: "95008",
|
zip: "95008",
|
||||||
|
@ -78,6 +87,7 @@ const testingScenariosMap = {
|
||||||
order: defaultOrder,
|
order: defaultOrder,
|
||||||
channelConfig: defaultChannelConfig,
|
channelConfig: defaultChannelConfig,
|
||||||
response: defaultOrderCreatedResponse,
|
response: defaultOrderCreatedResponse,
|
||||||
|
providerConfig: defaultProviderConfig,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -102,4 +112,10 @@ export class TaxJarOrderCreatedMockGenerator {
|
||||||
...testingScenariosMap[this.scenario].response,
|
...testingScenariosMap[this.scenario].response,
|
||||||
...overrides,
|
...overrides,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
generateProviderConfig = (overrides: Partial<TaxJarConfig> = {}): TaxJarConfig =>
|
||||||
|
structuredClone({
|
||||||
|
...testingScenariosMap[this.scenario].providerConfig,
|
||||||
|
...overrides,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,9 +11,9 @@ describe("TaxJarOrderCreatedPayloadTransformer", () => {
|
||||||
it("returns the correct order amount", () => {
|
it("returns the correct order amount", () => {
|
||||||
const payloadMock = {
|
const payloadMock = {
|
||||||
order: mockGenerator.generateOrder(),
|
order: mockGenerator.generateOrder(),
|
||||||
channelConfig: mockGenerator.generateChannelConfig(),
|
|
||||||
};
|
};
|
||||||
const transformer = new TaxJarOrderCreatedPayloadTransformer();
|
const providerConfig = mockGenerator.generateProviderConfig();
|
||||||
|
const transformer = new TaxJarOrderCreatedPayloadTransformer(providerConfig);
|
||||||
const transformedPayload = transformer.transform(payloadMock);
|
const transformedPayload = transformer.transform(payloadMock);
|
||||||
|
|
||||||
expect(transformedPayload.params.amount).toBe(239.17);
|
expect(transformedPayload.params.amount).toBe(239.17);
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
import { LineItem } from "taxjar/dist/util/types";
|
import { LineItem } from "taxjar/dist/util/types";
|
||||||
import { OrderCreatedSubscriptionFragment } from "../../../../generated/graphql";
|
import { OrderCreatedSubscriptionFragment } from "../../../../generated/graphql";
|
||||||
import { numbers } from "../../taxes/numbers";
|
import { numbers } from "../../taxes/numbers";
|
||||||
import { Payload, Target } from "./taxjar-order-created-adapter";
|
|
||||||
import { taxProviderUtils } from "../../taxes/tax-provider-utils";
|
import { taxProviderUtils } from "../../taxes/tax-provider-utils";
|
||||||
|
import { TaxJarConfig } from "../taxjar-config";
|
||||||
|
import {
|
||||||
|
TaxJarOrderCreatedPayload,
|
||||||
|
TaxJarOrderCreatedTarget,
|
||||||
|
} from "./taxjar-order-created-adapter";
|
||||||
|
|
||||||
export function sumPayloadLines(lines: LineItem[]): number {
|
export function sumPayloadLines(lines: LineItem[]): number {
|
||||||
return numbers.roundFloatToTwoDecimals(
|
return numbers.roundFloatToTwoDecimals(
|
||||||
|
@ -23,6 +27,7 @@ export function sumPayloadLines(lines: LineItem[]): number {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TaxJarOrderCreatedPayloadTransformer {
|
export class TaxJarOrderCreatedPayloadTransformer {
|
||||||
|
constructor(private readonly config: TaxJarConfig) {}
|
||||||
private mapLines(lines: OrderCreatedSubscriptionFragment["lines"]): LineItem[] {
|
private mapLines(lines: OrderCreatedSubscriptionFragment["lines"]): LineItem[] {
|
||||||
return lines.map((line) => ({
|
return lines.map((line) => ({
|
||||||
quantity: line.quantity,
|
quantity: line.quantity,
|
||||||
|
@ -35,7 +40,7 @@ export class TaxJarOrderCreatedPayloadTransformer {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
transform({ order, channelConfig }: Payload): Target {
|
transform({ order }: TaxJarOrderCreatedPayload): TaxJarOrderCreatedTarget {
|
||||||
const lineItems = this.mapLines(order.lines);
|
const lineItems = this.mapLines(order.lines);
|
||||||
const lineSum = sumPayloadLines(lineItems);
|
const lineSum = sumPayloadLines(lineItems);
|
||||||
const shippingAmount = order.shippingPrice.gross.amount;
|
const shippingAmount = order.shippingPrice.gross.amount;
|
||||||
|
@ -47,11 +52,11 @@ export class TaxJarOrderCreatedPayloadTransformer {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
params: {
|
params: {
|
||||||
from_country: channelConfig.address.country,
|
from_country: this.config.address.country,
|
||||||
from_zip: channelConfig.address.zip,
|
from_zip: this.config.address.zip,
|
||||||
from_state: channelConfig.address.state,
|
from_state: this.config.address.state,
|
||||||
from_city: channelConfig.address.city,
|
from_city: this.config.address.city,
|
||||||
from_street: channelConfig.address.street,
|
from_street: this.config.address.street,
|
||||||
to_country: order.shippingAddress!.country.code,
|
to_country: order.shippingAddress!.country.code,
|
||||||
to_zip: order.shippingAddress!.postalCode,
|
to_zip: order.shippingAddress!.postalCode,
|
||||||
to_state: order.shippingAddress!.countryArea,
|
to_state: order.shippingAddress!.countryArea,
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { TaxJarConfig } from "./taxjar-config";
|
||||||
|
|
||||||
const createTaxJarSettings = (config: TaxJarConfig): Config => {
|
const createTaxJarSettings = (config: TaxJarConfig): Config => {
|
||||||
const settings: Config = {
|
const settings: Config = {
|
||||||
apiKey: config.apiKey,
|
apiKey: config.credentials.apiKey,
|
||||||
apiUrl: config.isSandbox ? TaxJar.SANDBOX_API_URL : TaxJar.DEFAULT_API_URL,
|
apiUrl: config.isSandbox ? TaxJar.SANDBOX_API_URL : TaxJar.DEFAULT_API_URL,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,39 @@
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { obfuscateSecret } from "../../lib/utils";
|
import { obfuscateSecret } from "../../lib/utils";
|
||||||
|
|
||||||
|
const addressSchema = z.object({
|
||||||
|
country: z.string(),
|
||||||
|
zip: z.string(),
|
||||||
|
state: z.string(),
|
||||||
|
city: z.string(),
|
||||||
|
street: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const taxJarCredentialsSchema = z.object({
|
||||||
|
apiKey: z.string().min(1, { message: "API Key requires at least one character." }),
|
||||||
|
});
|
||||||
|
|
||||||
export const taxJarConfigSchema = z.object({
|
export const taxJarConfigSchema = z.object({
|
||||||
name: z.string().min(1, { message: "Name requires at least one character." }),
|
name: z.string().min(1, { message: "Name requires at least one character." }),
|
||||||
apiKey: z.string().min(1, { message: "API Key requires at least one character." }),
|
|
||||||
isSandbox: z.boolean(),
|
isSandbox: z.boolean(),
|
||||||
|
credentials: taxJarCredentialsSchema,
|
||||||
|
address: addressSchema,
|
||||||
});
|
});
|
||||||
export type TaxJarConfig = z.infer<typeof taxJarConfigSchema>;
|
export type TaxJarConfig = z.infer<typeof taxJarConfigSchema>;
|
||||||
|
|
||||||
export const defaultTaxJarConfig: TaxJarConfig = {
|
export const defaultTaxJarConfig: TaxJarConfig = {
|
||||||
name: "",
|
name: "",
|
||||||
apiKey: "",
|
|
||||||
isSandbox: false,
|
isSandbox: false,
|
||||||
|
credentials: {
|
||||||
|
apiKey: "",
|
||||||
|
},
|
||||||
|
address: {
|
||||||
|
city: "",
|
||||||
|
country: "",
|
||||||
|
state: "",
|
||||||
|
street: "",
|
||||||
|
zip: "",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const taxJarInstanceConfigSchema = z.object({
|
export const taxJarInstanceConfigSchema = z.object({
|
||||||
|
@ -22,12 +44,17 @@ export const taxJarInstanceConfigSchema = z.object({
|
||||||
|
|
||||||
export type TaxJarInstanceConfig = z.infer<typeof taxJarInstanceConfigSchema>;
|
export type TaxJarInstanceConfig = z.infer<typeof taxJarInstanceConfigSchema>;
|
||||||
|
|
||||||
export const obfuscateTaxJarConfig = (config: TaxJarConfig) => ({
|
export const obfuscateTaxJarConfig = (config: TaxJarConfig): TaxJarConfig => ({
|
||||||
...config,
|
...config,
|
||||||
apiKey: obfuscateSecret(config.apiKey),
|
credentials: {
|
||||||
|
...config.credentials,
|
||||||
|
apiKey: obfuscateSecret(config.credentials.apiKey),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const obfuscateTaxJarInstances = (instances: TaxJarInstanceConfig[]) =>
|
export const obfuscateTaxJarInstances = (
|
||||||
|
instances: TaxJarInstanceConfig[]
|
||||||
|
): TaxJarInstanceConfig[] =>
|
||||||
instances.map((instance) => ({
|
instances.map((instance) => ({
|
||||||
...instance,
|
...instance,
|
||||||
config: obfuscateTaxJarConfig(instance.config),
|
config: obfuscateTaxJarConfig(instance.config),
|
||||||
|
|
|
@ -17,11 +17,15 @@ const deleteInputSchema = z.object({
|
||||||
const patchInputSchema = z.object({
|
const patchInputSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
value: taxJarConfigSchema.partial().transform((c) => {
|
value: taxJarConfigSchema.partial().transform((c) => {
|
||||||
const { apiKey, ...config } = c ?? {};
|
const { credentials, ...config } = c ?? {};
|
||||||
|
const { apiKey } = credentials ?? {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...config,
|
...config,
|
||||||
...(apiKey && !isObfuscated(apiKey) && { apiKey }),
|
credentials: {
|
||||||
|
...credentials,
|
||||||
|
...(apiKey && !isObfuscated(apiKey) && { apiKey }),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { providersSchema } from "../providers-configuration/providers-config";
|
||||||
import { TAX_PROVIDER_KEY } from "../providers-configuration/public-providers-configuration-service";
|
import { TAX_PROVIDER_KEY } from "../providers-configuration/public-providers-configuration-service";
|
||||||
import { TaxJarClient } from "./taxjar-client";
|
import { TaxJarClient } from "./taxjar-client";
|
||||||
import { TaxJarConfig, TaxJarInstanceConfig, taxJarInstanceConfigSchema } from "./taxjar-config";
|
import { TaxJarConfig, TaxJarInstanceConfig, taxJarInstanceConfigSchema } from "./taxjar-config";
|
||||||
|
import { DeepPartial } from "@trpc/server";
|
||||||
|
|
||||||
const getSchema = taxJarInstanceConfigSchema;
|
const getSchema = taxJarInstanceConfigSchema;
|
||||||
|
|
||||||
|
@ -78,7 +79,7 @@ export class TaxJarConfigurationService {
|
||||||
return result.data;
|
return result.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async patch(id: string, config: Partial<TaxJarConfig>): Promise<void> {
|
async patch(id: string, config: DeepPartial<TaxJarConfig>): Promise<void> {
|
||||||
this.logger.debug(`.patch called with id: ${id} and value: ${JSON.stringify(config)}`);
|
this.logger.debug(`.patch called with id: ${id} and value: ${JSON.stringify(config)}`);
|
||||||
const data = await this.get(id);
|
const data = await this.get(id);
|
||||||
// omit the key "id" from the result
|
// omit the key "id" from the result
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
import { OrderCreatedSubscriptionFragment, TaxBaseFragment } from "../../../generated/graphql";
|
import { OrderCreatedSubscriptionFragment, TaxBaseFragment } from "../../../generated/graphql";
|
||||||
import { Logger, createLogger } from "../../lib/logger";
|
import { Logger, createLogger } from "../../lib/logger";
|
||||||
import { ChannelConfig } from "../channels-configuration/channels-config";
|
|
||||||
import { ProviderWebhookService } from "../taxes/tax-provider-webhook";
|
|
||||||
import { TaxJarCalculateTaxesAdapter } from "./calculate-taxes/taxjar-calculate-taxes-adapter";
|
import { TaxJarCalculateTaxesAdapter } from "./calculate-taxes/taxjar-calculate-taxes-adapter";
|
||||||
import { TaxJarClient } from "./taxjar-client";
|
import { TaxJarClient } from "./taxjar-client";
|
||||||
import { TaxJarConfig } from "./taxjar-config";
|
import { TaxJarConfig } from "./taxjar-config";
|
||||||
import { TaxJarOrderCreatedAdapter } from "./order-created/taxjar-order-created-adapter";
|
import { TaxJarOrderCreatedAdapter } from "./order-created/taxjar-order-created-adapter";
|
||||||
|
import { ProviderWebhookService } from "../taxes/tax-provider-webhook";
|
||||||
|
|
||||||
export class TaxJarWebhookService implements ProviderWebhookService {
|
export class TaxJarWebhookService implements ProviderWebhookService {
|
||||||
client: TaxJarClient;
|
client: TaxJarClient;
|
||||||
config: TaxJarConfig;
|
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
private config: TaxJarConfig;
|
||||||
|
|
||||||
constructor(config: TaxJarConfig) {
|
constructor(config: TaxJarConfig) {
|
||||||
const taxJarClient = new TaxJarClient(config);
|
const taxJarClient = new TaxJarClient(config);
|
||||||
|
@ -22,22 +21,26 @@ export class TaxJarWebhookService implements ProviderWebhookService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async calculateTaxes(taxBase: TaxBaseFragment, channelConfig: ChannelConfig) {
|
async calculateTaxes(taxBase: TaxBaseFragment) {
|
||||||
this.logger.debug({ taxBase, channelConfig }, "calculateTaxes called with:");
|
const providerConfig = this.config;
|
||||||
|
|
||||||
|
this.logger.debug({ taxBase, providerConfig }, "calculateTaxes called with:");
|
||||||
const adapter = new TaxJarCalculateTaxesAdapter(this.config);
|
const adapter = new TaxJarCalculateTaxesAdapter(this.config);
|
||||||
|
|
||||||
const response = await adapter.send({ channelConfig, taxBase });
|
const response = await adapter.send({ taxBase });
|
||||||
|
|
||||||
this.logger.debug({ response }, "calculateTaxes response:");
|
this.logger.debug({ response }, "calculateTaxes response:");
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOrder(order: OrderCreatedSubscriptionFragment, channelConfig: ChannelConfig) {
|
async createOrder(order: OrderCreatedSubscriptionFragment) {
|
||||||
this.logger.debug({ order, channelConfig }, "createOrder called with:");
|
const providerConfig = this.config;
|
||||||
|
|
||||||
|
this.logger.debug({ order, providerConfig }, "createOrder called with:");
|
||||||
|
|
||||||
const adapter = new TaxJarOrderCreatedAdapter(this.config);
|
const adapter = new TaxJarOrderCreatedAdapter(this.config);
|
||||||
|
|
||||||
const response = await adapter.send({ channelConfig, order });
|
const response = await adapter.send({ order });
|
||||||
|
|
||||||
this.logger.debug({ response }, "createOrder response:");
|
this.logger.debug({ response }, "createOrder response:");
|
||||||
return response;
|
return response;
|
||||||
|
|
|
@ -1,230 +0,0 @@
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import {
|
|
||||||
FormHelperText,
|
|
||||||
Grid,
|
|
||||||
InputLabel,
|
|
||||||
Switch,
|
|
||||||
TextField,
|
|
||||||
TextFieldProps,
|
|
||||||
} from "@material-ui/core";
|
|
||||||
import { Delete, Save } from "@material-ui/icons";
|
|
||||||
import { Button, makeStyles } from "@saleor/macaw-ui";
|
|
||||||
import React from "react";
|
|
||||||
import { Controller, useForm } from "react-hook-form";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { useInstanceId } from "../../taxes/tax-context";
|
|
||||||
import { trpcClient } from "../../trpc/trpc-client";
|
|
||||||
import { taxJarConfigSchema } from "../taxjar-config";
|
|
||||||
import { useDashboardNotification } from "@saleor/apps-shared";
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
|
||||||
reverseRow: {
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "row-reverse",
|
|
||||||
gap: theme.spacing(1),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const schema = taxJarConfigSchema;
|
|
||||||
|
|
||||||
type FormValues = z.infer<typeof schema>;
|
|
||||||
|
|
||||||
const defaultValues: FormValues = {
|
|
||||||
name: "",
|
|
||||||
apiKey: "",
|
|
||||||
isSandbox: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const TaxJarConfigurationForm = () => {
|
|
||||||
const [isWarningDialogOpen, setIsWarningDialogOpen] = React.useState(false);
|
|
||||||
const styles = useStyles();
|
|
||||||
const { instanceId, setInstanceId } = useInstanceId();
|
|
||||||
const { handleSubmit, reset, control, formState } = useForm<FormValues>({
|
|
||||||
resolver: zodResolver(schema),
|
|
||||||
defaultValues,
|
|
||||||
});
|
|
||||||
const { notifySuccess, notifyError } = useDashboardNotification();
|
|
||||||
|
|
||||||
const resetInstanceId = () => {
|
|
||||||
setInstanceId(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const { refetch: refetchChannelConfigurationData } =
|
|
||||||
trpcClient.channelsConfiguration.fetch.useQuery(undefined, {
|
|
||||||
onError(error) {
|
|
||||||
notifyError("Error", error.message);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { refetch: refetchProvidersConfigurationData } =
|
|
||||||
trpcClient.providersConfiguration.getAll.useQuery();
|
|
||||||
const { data: instance } = trpcClient.taxJarConfiguration.get.useQuery(
|
|
||||||
{ id: instanceId ?? "" },
|
|
||||||
{
|
|
||||||
enabled: !!instanceId,
|
|
||||||
onError(error) {
|
|
||||||
notifyError("Error", error.message);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const { mutate: createMutation, isLoading: isCreateLoading } =
|
|
||||||
trpcClient.taxJarConfiguration.post.useMutation({
|
|
||||||
onSuccess({ id }) {
|
|
||||||
setInstanceId(id);
|
|
||||||
refetchProvidersConfigurationData();
|
|
||||||
refetchChannelConfigurationData();
|
|
||||||
|
|
||||||
notifySuccess("Success", "Saved TaxJar configuration");
|
|
||||||
},
|
|
||||||
onError(error) {
|
|
||||||
notifyError("Error", error.message);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { mutate: updateMutation, isLoading: isUpdateLoading } =
|
|
||||||
trpcClient.taxJarConfiguration.patch.useMutation({
|
|
||||||
onSuccess() {
|
|
||||||
refetchProvidersConfigurationData();
|
|
||||||
refetchChannelConfigurationData();
|
|
||||||
notifySuccess("Success", "Updated TaxJar configuration");
|
|
||||||
},
|
|
||||||
onError(error) {
|
|
||||||
notifyError("Error", error.message);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { mutate: deleteMutation, isLoading: isDeleteLoading } =
|
|
||||||
trpcClient.taxJarConfiguration.delete.useMutation({
|
|
||||||
onSuccess() {
|
|
||||||
resetInstanceId();
|
|
||||||
refetchProvidersConfigurationData();
|
|
||||||
refetchChannelConfigurationData();
|
|
||||||
|
|
||||||
notifySuccess("Success", "Removed TaxJar instance");
|
|
||||||
},
|
|
||||||
onError(error) {
|
|
||||||
notifyError("Error", error.message);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (instance) {
|
|
||||||
const { config } = instance;
|
|
||||||
|
|
||||||
reset(config);
|
|
||||||
} else {
|
|
||||||
reset({ ...defaultValues });
|
|
||||||
}
|
|
||||||
}, [instance, instanceId, reset]);
|
|
||||||
|
|
||||||
const textFieldProps: TextFieldProps = {
|
|
||||||
fullWidth: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSubmit = (value: FormValues) => {
|
|
||||||
if (instanceId) {
|
|
||||||
updateMutation({
|
|
||||||
id: instanceId,
|
|
||||||
value,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
createMutation({
|
|
||||||
value,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeWarningDialog = () => {
|
|
||||||
setIsWarningDialogOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const openWarningDialog = () => {
|
|
||||||
setIsWarningDialogOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteProvider = () => {
|
|
||||||
closeWarningDialog();
|
|
||||||
if (instanceId) {
|
|
||||||
deleteMutation({ id: instanceId });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const isLoading = isCreateLoading || isUpdateLoading;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
|
||||||
<Grid container spacing={2}>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Controller
|
|
||||||
name="name"
|
|
||||||
control={control}
|
|
||||||
defaultValue={defaultValues.name}
|
|
||||||
render={({ field }) => (
|
|
||||||
<TextField
|
|
||||||
required
|
|
||||||
type="text"
|
|
||||||
{...field}
|
|
||||||
label="Instance name"
|
|
||||||
{...textFieldProps}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{formState.errors.name && (
|
|
||||||
<FormHelperText error>{formState.errors.name.message}</FormHelperText>
|
|
||||||
)}
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Controller
|
|
||||||
name="apiKey"
|
|
||||||
control={control}
|
|
||||||
defaultValue={defaultValues.apiKey}
|
|
||||||
render={({ field }) => (
|
|
||||||
<TextField required label="API Key" {...field} {...textFieldProps} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{formState.errors?.apiKey && (
|
|
||||||
<FormHelperText error>{formState.errors?.apiKey.message}</FormHelperText>
|
|
||||||
)}
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<InputLabel>
|
|
||||||
Sandbox
|
|
||||||
<Controller
|
|
||||||
name={"isSandbox"}
|
|
||||||
control={control}
|
|
||||||
defaultValue={defaultValues.isSandbox}
|
|
||||||
render={({ field }) => (
|
|
||||||
<Switch
|
|
||||||
{...field}
|
|
||||||
checked={field.value}
|
|
||||||
onChange={(e) => field.onChange(e.target.checked)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</InputLabel>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
<br />
|
|
||||||
<div className={styles.reverseRow}>
|
|
||||||
<Button startIcon={<Save />} type="submit" variant="primary">
|
|
||||||
{isLoading ? "Saving..." : "Save"}
|
|
||||||
</Button>
|
|
||||||
{instanceId && (
|
|
||||||
<Button onClick={deleteProvider} startIcon={<Delete />}>
|
|
||||||
{isDeleteLoading ? "Deleting..." : "Delete"}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{/* // todo: bring back to life once Dashboard allows to summon dialog */}
|
|
||||||
{/* <DeleteProviderDialog
|
|
||||||
isOpen={isWarningDialogOpen}
|
|
||||||
onClose={closeWarningDialog}
|
|
||||||
onCancel={closeWarningDialog}
|
|
||||||
onConfirm={deleteProvider}
|
|
||||||
/> */}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,10 +0,0 @@
|
||||||
import { TaxJarConfigurationForm } from "./taxjar-configuration-form";
|
|
||||||
|
|
||||||
export const TaxJarConfiguration = () => {
|
|
||||||
return (
|
|
||||||
<section>
|
|
||||||
<h2>TaxJar configuration</h2>
|
|
||||||
<TaxJarConfigurationForm />
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
65
apps/taxes/src/modules/ui/app-breadcrumbs.tsx
Normal file
65
apps/taxes/src/modules/ui/app-breadcrumbs.tsx
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
import { Breadcrumbs } from "@saleor/apps-ui";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
type Breadcrumb = {
|
||||||
|
label: string;
|
||||||
|
href?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const newProviderBreadcrumbs: Breadcrumb[] = [
|
||||||
|
{
|
||||||
|
href: "/configuration",
|
||||||
|
label: "Configuration",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Add provider",
|
||||||
|
href: "/providers",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const breadcrumbsForRoute: Record<string, Breadcrumb[]> = {
|
||||||
|
"/configuration": [
|
||||||
|
{
|
||||||
|
href: "/configuration",
|
||||||
|
label: "Configuration",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"/providers": [...newProviderBreadcrumbs],
|
||||||
|
"/providers/taxjar": [
|
||||||
|
...newProviderBreadcrumbs,
|
||||||
|
{
|
||||||
|
label: "TaxJar",
|
||||||
|
href: "/providers/taxjar",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"/providers/avatax": [
|
||||||
|
...newProviderBreadcrumbs,
|
||||||
|
{
|
||||||
|
label: "Avatax",
|
||||||
|
href: "/providers/avatax",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const useBreadcrumbs = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const breadcrumbs = breadcrumbsForRoute[router.pathname];
|
||||||
|
|
||||||
|
if (!breadcrumbs) {
|
||||||
|
throw new Error(`No breadcrumbs for route ${router.pathname}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return breadcrumbs;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AppBreadcrumbs = () => {
|
||||||
|
const breadcrumbs = useBreadcrumbs();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Breadcrumbs>
|
||||||
|
{breadcrumbs.map((breadcrumb) => (
|
||||||
|
<Breadcrumbs.Item href={breadcrumb.href}>{breadcrumb.label}</Breadcrumbs.Item>
|
||||||
|
))}
|
||||||
|
</Breadcrumbs>
|
||||||
|
);
|
||||||
|
};
|
16
apps/taxes/src/modules/ui/app-card.tsx
Normal file
16
apps/taxes/src/modules/ui/app-card.tsx
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { PropsWithBox, Box } from "@saleor/macaw-ui/next";
|
||||||
|
|
||||||
|
export const AppCard = ({ children, ...p }: PropsWithBox<{}>) => {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
borderRadius={4}
|
||||||
|
borderWidth={1}
|
||||||
|
borderColor={"neutralPlain"}
|
||||||
|
borderStyle={"solid"}
|
||||||
|
padding={8}
|
||||||
|
{...p}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
33
apps/taxes/src/modules/ui/app-columns.tsx
Normal file
33
apps/taxes/src/modules/ui/app-columns.tsx
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import { Box } from "@saleor/macaw-ui/next";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const ratioMap = {
|
||||||
|
"1/1": "1fr 1fr",
|
||||||
|
"1/2": "1fr 2fr",
|
||||||
|
};
|
||||||
|
|
||||||
|
type Ratio = keyof typeof ratioMap;
|
||||||
|
|
||||||
|
type AppColumnsProps = {
|
||||||
|
top: React.ReactNode;
|
||||||
|
bottomLeft: React.ReactNode;
|
||||||
|
bottomRight: React.ReactNode;
|
||||||
|
gridRatio?: Ratio;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AppColumns = ({
|
||||||
|
top,
|
||||||
|
bottomLeft,
|
||||||
|
bottomRight,
|
||||||
|
gridRatio = "1/1",
|
||||||
|
}: AppColumnsProps) => {
|
||||||
|
return (
|
||||||
|
<Box display={"grid"} __gap={"60px"}>
|
||||||
|
<Box>{top}</Box>
|
||||||
|
<Box display={"grid"} gap={10} __gridTemplateColumns={ratioMap[gridRatio]}>
|
||||||
|
<Box>{bottomLeft}</Box>
|
||||||
|
<Box>{bottomRight}</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,14 +0,0 @@
|
||||||
import { makeStyles } from "@saleor/macaw-ui";
|
|
||||||
|
|
||||||
export const useStyles = makeStyles({
|
|
||||||
root: {
|
|
||||||
maxWidth: 1180,
|
|
||||||
margin: "0 auto",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const AppContainer = ({ children }: { children: React.ReactNode }) => {
|
|
||||||
const styles = useStyles();
|
|
||||||
|
|
||||||
return <div className={styles.root}>{children}</div>;
|
|
||||||
};
|
|
|
@ -1,19 +0,0 @@
|
||||||
import { makeStyles } from "@saleor/macaw-ui";
|
|
||||||
import { PropsWithChildren } from "react";
|
|
||||||
|
|
||||||
export const useStyles = makeStyles({
|
|
||||||
root: {
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "280px auto 280px",
|
|
||||||
alignItems: "start",
|
|
||||||
gap: 32,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export type Props = PropsWithChildren<{}>;
|
|
||||||
|
|
||||||
export const AppGrid = ({ children }: Props) => {
|
|
||||||
const styles = useStyles();
|
|
||||||
|
|
||||||
return <div className={styles.root}>{children}</div>;
|
|
||||||
};
|
|
|
@ -1,11 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import { AppContainer } from "./app-container";
|
|
||||||
import { AppGrid } from "./app-grid";
|
|
||||||
|
|
||||||
export const AppLayout = ({ children }: { children: React.ReactNode }) => {
|
|
||||||
return (
|
|
||||||
<AppContainer>
|
|
||||||
<AppGrid>{children}</AppGrid>
|
|
||||||
</AppContainer>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,13 +0,0 @@
|
||||||
import { Link } from "@material-ui/core";
|
|
||||||
import { PropsWithChildren } from "react";
|
|
||||||
import { useAppRedirect } from "../../lib/app/redirect";
|
|
||||||
|
|
||||||
export const AppLink = ({ children, href }: PropsWithChildren<{ href: string }>) => {
|
|
||||||
const { redirect } = useAppRedirect();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link target={"_blank"} href={href} rel="noreferrer" onClick={() => redirect(href)}>
|
|
||||||
{children}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,19 +0,0 @@
|
||||||
import { Paper } from "@material-ui/core";
|
|
||||||
import { makeStyles } from "@saleor/macaw-ui";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
|
||||||
root: {
|
|
||||||
padding: "16px",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const AppPaper = ({ children }: { children: React.ReactNode }) => {
|
|
||||||
const styles = useStyles();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Paper elevation={0} className={styles.root}>
|
|
||||||
{children}
|
|
||||||
</Paper>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,23 +0,0 @@
|
||||||
import { Link, makeStyles } from "@material-ui/core";
|
|
||||||
import { PropsWithChildren } from "react";
|
|
||||||
import { AppTab } from "../../pages/configuration";
|
|
||||||
import { useActiveTab } from "../taxes/tax-context";
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
|
||||||
button: {
|
|
||||||
fontSize: "inherit",
|
|
||||||
fontFamily: "inherit",
|
|
||||||
verticalAlign: "unset",
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const AppTabNavButton = ({ children, to }: PropsWithChildren<{ to: AppTab }>) => {
|
|
||||||
const styles = useStyles();
|
|
||||||
const { setActiveTab } = useActiveTab();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link className={styles.button} component="button" onClick={() => setActiveTab(to)}>
|
|
||||||
{children}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,382 +0,0 @@
|
||||||
type CountryType = {
|
|
||||||
code: string;
|
|
||||||
label: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
// From https://bitbucket.org/atlassian/atlaskit-mk-2/raw/4ad0e56649c3e6c973e226b7efaeb28cb240ccb0/packages/core/select/src/data/countries.js
|
|
||||||
export const countries: CountryType[] = [
|
|
||||||
{ code: "AD", label: "Andorra" },
|
|
||||||
{
|
|
||||||
code: "AE",
|
|
||||||
label: "United Arab Emirates",
|
|
||||||
},
|
|
||||||
{ code: "AF", label: "Afghanistan" },
|
|
||||||
{
|
|
||||||
code: "AG",
|
|
||||||
label: "Antigua and Barbuda",
|
|
||||||
},
|
|
||||||
{ code: "AI", label: "Anguilla" },
|
|
||||||
{ code: "AL", label: "Albania" },
|
|
||||||
{ code: "AM", label: "Armenia" },
|
|
||||||
{ code: "AO", label: "Angola" },
|
|
||||||
{ code: "AQ", label: "Antarctica" },
|
|
||||||
{ code: "AR", label: "Argentina" },
|
|
||||||
{ code: "AS", label: "American Samoa" },
|
|
||||||
{ code: "AT", label: "Austria" },
|
|
||||||
{
|
|
||||||
code: "AU",
|
|
||||||
label: "Australia",
|
|
||||||
},
|
|
||||||
{ code: "AW", label: "Aruba" },
|
|
||||||
{ code: "AX", label: "Alland Islands" },
|
|
||||||
{ code: "AZ", label: "Azerbaijan" },
|
|
||||||
{
|
|
||||||
code: "BA",
|
|
||||||
label: "Bosnia and Herzegovina",
|
|
||||||
},
|
|
||||||
{ code: "BB", label: "Barbados" },
|
|
||||||
{ code: "BD", label: "Bangladesh" },
|
|
||||||
{ code: "BE", label: "Belgium" },
|
|
||||||
{ code: "BF", label: "Burkina Faso" },
|
|
||||||
{ code: "BG", label: "Bulgaria" },
|
|
||||||
{ code: "BH", label: "Bahrain" },
|
|
||||||
{ code: "BI", label: "Burundi" },
|
|
||||||
{ code: "BJ", label: "Benin" },
|
|
||||||
{ code: "BL", label: "Saint Barthelemy" },
|
|
||||||
{ code: "BM", label: "Bermuda" },
|
|
||||||
{ code: "BN", label: "Brunei Darussalam" },
|
|
||||||
{ code: "BO", label: "Bolivia" },
|
|
||||||
{ code: "BR", label: "Brazil" },
|
|
||||||
{ code: "BS", label: "Bahamas" },
|
|
||||||
{ code: "BT", label: "Bhutan" },
|
|
||||||
{ code: "BV", label: "Bouvet Island" },
|
|
||||||
{ code: "BW", label: "Botswana" },
|
|
||||||
{ code: "BY", label: "Belarus" },
|
|
||||||
{ code: "BZ", label: "Belize" },
|
|
||||||
{
|
|
||||||
code: "CA",
|
|
||||||
label: "Canada",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
code: "CC",
|
|
||||||
label: "Cocos (Keeling) Islands",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
code: "CD",
|
|
||||||
label: "Congo, Democratic Republic of the",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
code: "CF",
|
|
||||||
label: "Central African Republic",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
code: "CG",
|
|
||||||
label: "Congo, Republic of the",
|
|
||||||
},
|
|
||||||
{ code: "CH", label: "Switzerland" },
|
|
||||||
{ code: "CI", label: "Cote d'Ivoire" },
|
|
||||||
{ code: "CK", label: "Cook Islands" },
|
|
||||||
{ code: "CL", label: "Chile" },
|
|
||||||
{ code: "CM", label: "Cameroon" },
|
|
||||||
{ code: "CN", label: "China" },
|
|
||||||
{ code: "CO", label: "Colombia" },
|
|
||||||
{ code: "CR", label: "Costa Rica" },
|
|
||||||
{ code: "CU", label: "Cuba" },
|
|
||||||
{ code: "CV", label: "Cape Verde" },
|
|
||||||
{ code: "CW", label: "Curacao" },
|
|
||||||
{ code: "CX", label: "Christmas Island" },
|
|
||||||
{ code: "CY", label: "Cyprus" },
|
|
||||||
{ code: "CZ", label: "Czech Republic" },
|
|
||||||
{
|
|
||||||
code: "DE",
|
|
||||||
label: "Germany",
|
|
||||||
},
|
|
||||||
{ code: "DJ", label: "Djibouti" },
|
|
||||||
{ code: "DK", label: "Denmark" },
|
|
||||||
{ code: "DM", label: "Dominica" },
|
|
||||||
{
|
|
||||||
code: "DO",
|
|
||||||
label: "Dominican Republic",
|
|
||||||
},
|
|
||||||
{ code: "DZ", label: "Algeria" },
|
|
||||||
{ code: "EC", label: "Ecuador" },
|
|
||||||
{ code: "EE", label: "Estonia" },
|
|
||||||
{ code: "EG", label: "Egypt" },
|
|
||||||
{ code: "EH", label: "Western Sahara" },
|
|
||||||
{ code: "ER", label: "Eritrea" },
|
|
||||||
{ code: "ES", label: "Spain" },
|
|
||||||
{ code: "ET", label: "Ethiopia" },
|
|
||||||
{ code: "FI", label: "Finland" },
|
|
||||||
{ code: "FJ", label: "Fiji" },
|
|
||||||
{
|
|
||||||
code: "FK",
|
|
||||||
label: "Falkland Islands (Malvinas)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
code: "FM",
|
|
||||||
label: "Micronesia, Federated States of",
|
|
||||||
},
|
|
||||||
{ code: "FO", label: "Faroe Islands" },
|
|
||||||
{
|
|
||||||
code: "FR",
|
|
||||||
label: "France",
|
|
||||||
},
|
|
||||||
{ code: "GA", label: "Gabon" },
|
|
||||||
{ code: "GB", label: "United Kingdom" },
|
|
||||||
{ code: "GD", label: "Grenada" },
|
|
||||||
{ code: "GE", label: "Georgia" },
|
|
||||||
{ code: "GF", label: "French Guiana" },
|
|
||||||
{ code: "GG", label: "Guernsey" },
|
|
||||||
{ code: "GH", label: "Ghana" },
|
|
||||||
{ code: "GI", label: "Gibraltar" },
|
|
||||||
{ code: "GL", label: "Greenland" },
|
|
||||||
{ code: "GM", label: "Gambia" },
|
|
||||||
{ code: "GN", label: "Guinea" },
|
|
||||||
{ code: "GP", label: "Guadeloupe" },
|
|
||||||
{ code: "GQ", label: "Equatorial Guinea" },
|
|
||||||
{ code: "GR", label: "Greece" },
|
|
||||||
{
|
|
||||||
code: "GS",
|
|
||||||
label: "South Georgia and the South Sandwich Islands",
|
|
||||||
},
|
|
||||||
{ code: "GT", label: "Guatemala" },
|
|
||||||
{ code: "GU", label: "Guam" },
|
|
||||||
{ code: "GW", label: "Guinea-Bissau" },
|
|
||||||
{ code: "GY", label: "Guyana" },
|
|
||||||
{ code: "HK", label: "Hong Kong" },
|
|
||||||
{
|
|
||||||
code: "HM",
|
|
||||||
label: "Heard Island and McDonald Islands",
|
|
||||||
},
|
|
||||||
{ code: "HN", label: "Honduras" },
|
|
||||||
{ code: "HR", label: "Croatia" },
|
|
||||||
{ code: "HT", label: "Haiti" },
|
|
||||||
{ code: "HU", label: "Hungary" },
|
|
||||||
{ code: "ID", label: "Indonesia" },
|
|
||||||
{ code: "IE", label: "Ireland" },
|
|
||||||
{ code: "IL", label: "Israel" },
|
|
||||||
{ code: "IM", label: "Isle of Man" },
|
|
||||||
{ code: "IN", label: "India" },
|
|
||||||
{
|
|
||||||
code: "IO",
|
|
||||||
label: "British Indian Ocean Territory",
|
|
||||||
},
|
|
||||||
{ code: "IQ", label: "Iraq" },
|
|
||||||
{
|
|
||||||
code: "IR",
|
|
||||||
label: "Iran, Islamic Republic of",
|
|
||||||
},
|
|
||||||
{ code: "IS", label: "Iceland" },
|
|
||||||
{ code: "IT", label: "Italy" },
|
|
||||||
{ code: "JE", label: "Jersey" },
|
|
||||||
{ code: "JM", label: "Jamaica" },
|
|
||||||
{ code: "JO", label: "Jordan" },
|
|
||||||
{
|
|
||||||
code: "JP",
|
|
||||||
label: "Japan",
|
|
||||||
},
|
|
||||||
{ code: "KE", label: "Kenya" },
|
|
||||||
{ code: "KG", label: "Kyrgyzstan" },
|
|
||||||
{ code: "KH", label: "Cambodia" },
|
|
||||||
{ code: "KI", label: "Kiribati" },
|
|
||||||
{ code: "KM", label: "Comoros" },
|
|
||||||
{
|
|
||||||
code: "KN",
|
|
||||||
label: "Saint Kitts and Nevis",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
code: "KP",
|
|
||||||
label: "Korea, Democratic People's Republic of",
|
|
||||||
},
|
|
||||||
{ code: "KR", label: "Korea, Republic of" },
|
|
||||||
{ code: "KW", label: "Kuwait" },
|
|
||||||
{ code: "KY", label: "Cayman Islands" },
|
|
||||||
{ code: "KZ", label: "Kazakhstan" },
|
|
||||||
{
|
|
||||||
code: "LA",
|
|
||||||
label: "Lao People's Democratic Republic",
|
|
||||||
},
|
|
||||||
{ code: "LB", label: "Lebanon" },
|
|
||||||
{ code: "LC", label: "Saint Lucia" },
|
|
||||||
{ code: "LI", label: "Liechtenstein" },
|
|
||||||
{ code: "LK", label: "Sri Lanka" },
|
|
||||||
{ code: "LR", label: "Liberia" },
|
|
||||||
{ code: "LS", label: "Lesotho" },
|
|
||||||
{ code: "LT", label: "Lithuania" },
|
|
||||||
{ code: "LU", label: "Luxembourg" },
|
|
||||||
{ code: "LV", label: "Latvia" },
|
|
||||||
{ code: "LY", label: "Libya" },
|
|
||||||
{ code: "MA", label: "Morocco" },
|
|
||||||
{ code: "MC", label: "Monaco" },
|
|
||||||
{
|
|
||||||
code: "MD",
|
|
||||||
label: "Moldova, Republic of",
|
|
||||||
},
|
|
||||||
{ code: "ME", label: "Montenegro" },
|
|
||||||
{
|
|
||||||
code: "MF",
|
|
||||||
label: "Saint Martin (French part)",
|
|
||||||
},
|
|
||||||
{ code: "MG", label: "Madagascar" },
|
|
||||||
{ code: "MH", label: "Marshall Islands" },
|
|
||||||
{
|
|
||||||
code: "MK",
|
|
||||||
label: "Macedonia, the Former Yugoslav Republic of",
|
|
||||||
},
|
|
||||||
{ code: "ML", label: "Mali" },
|
|
||||||
{ code: "MM", label: "Myanmar" },
|
|
||||||
{ code: "MN", label: "Mongolia" },
|
|
||||||
{ code: "MO", label: "Macao" },
|
|
||||||
{
|
|
||||||
code: "MP",
|
|
||||||
label: "Northern Mariana Islands",
|
|
||||||
},
|
|
||||||
{ code: "MQ", label: "Martinique" },
|
|
||||||
{ code: "MR", label: "Mauritania" },
|
|
||||||
{ code: "MS", label: "Montserrat" },
|
|
||||||
{ code: "MT", label: "Malta" },
|
|
||||||
{ code: "MU", label: "Mauritius" },
|
|
||||||
{ code: "MV", label: "Maldives" },
|
|
||||||
{ code: "MW", label: "Malawi" },
|
|
||||||
{ code: "MX", label: "Mexico" },
|
|
||||||
{ code: "MY", label: "Malaysia" },
|
|
||||||
{ code: "MZ", label: "Mozambique" },
|
|
||||||
{ code: "NA", label: "Namibia" },
|
|
||||||
{ code: "NC", label: "New Caledonia" },
|
|
||||||
{ code: "NE", label: "Niger" },
|
|
||||||
{ code: "NF", label: "Norfolk Island" },
|
|
||||||
{ code: "NG", label: "Nigeria" },
|
|
||||||
{ code: "NI", label: "Nicaragua" },
|
|
||||||
{ code: "NL", label: "Netherlands" },
|
|
||||||
{ code: "NO", label: "Norway" },
|
|
||||||
{ code: "NP", label: "Nepal" },
|
|
||||||
{ code: "NR", label: "Nauru" },
|
|
||||||
{ code: "NU", label: "Niue" },
|
|
||||||
{ code: "NZ", label: "New Zealand" },
|
|
||||||
{ code: "OM", label: "Oman" },
|
|
||||||
{ code: "PA", label: "Panama" },
|
|
||||||
{ code: "PE", label: "Peru" },
|
|
||||||
{ code: "PF", label: "French Polynesia" },
|
|
||||||
{ code: "PG", label: "Papua New Guinea" },
|
|
||||||
{ code: "PH", label: "Philippines" },
|
|
||||||
{ code: "PK", label: "Pakistan" },
|
|
||||||
{ code: "PL", label: "Poland" },
|
|
||||||
{
|
|
||||||
code: "PM",
|
|
||||||
label: "Saint Pierre and Miquelon",
|
|
||||||
},
|
|
||||||
{ code: "PN", label: "Pitcairn" },
|
|
||||||
{ code: "PR", label: "Puerto Rico" },
|
|
||||||
{
|
|
||||||
code: "PS",
|
|
||||||
label: "Palestine, State of",
|
|
||||||
},
|
|
||||||
{ code: "PT", label: "Portugal" },
|
|
||||||
{ code: "PW", label: "Palau" },
|
|
||||||
{ code: "PY", label: "Paraguay" },
|
|
||||||
{ code: "QA", label: "Qatar" },
|
|
||||||
{ code: "RE", label: "Reunion" },
|
|
||||||
{ code: "RO", label: "Romania" },
|
|
||||||
{ code: "RS", label: "Serbia" },
|
|
||||||
{ code: "RU", label: "Russian Federation" },
|
|
||||||
{ code: "RW", label: "Rwanda" },
|
|
||||||
{ code: "SA", label: "Saudi Arabia" },
|
|
||||||
{ code: "SB", label: "Solomon Islands" },
|
|
||||||
{ code: "SC", label: "Seychelles" },
|
|
||||||
{ code: "SD", label: "Sudan" },
|
|
||||||
{ code: "SE", label: "Sweden" },
|
|
||||||
{ code: "SG", label: "Singapore" },
|
|
||||||
{ code: "SH", label: "Saint Helena" },
|
|
||||||
{ code: "SI", label: "Slovenia" },
|
|
||||||
{
|
|
||||||
code: "SJ",
|
|
||||||
label: "Svalbard and Jan Mayen",
|
|
||||||
},
|
|
||||||
{ code: "SK", label: "Slovakia" },
|
|
||||||
{ code: "SL", label: "Sierra Leone" },
|
|
||||||
{ code: "SM", label: "San Marino" },
|
|
||||||
{ code: "SN", label: "Senegal" },
|
|
||||||
{ code: "SO", label: "Somalia" },
|
|
||||||
{ code: "SR", label: "Suriname" },
|
|
||||||
{ code: "SS", label: "South Sudan" },
|
|
||||||
{
|
|
||||||
code: "ST",
|
|
||||||
label: "Sao Tome and Principe",
|
|
||||||
},
|
|
||||||
{ code: "SV", label: "El Salvador" },
|
|
||||||
{
|
|
||||||
code: "SX",
|
|
||||||
label: "Sint Maarten (Dutch part)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
code: "SY",
|
|
||||||
label: "Syrian Arab Republic",
|
|
||||||
},
|
|
||||||
{ code: "SZ", label: "Swaziland" },
|
|
||||||
{
|
|
||||||
code: "TC",
|
|
||||||
label: "Turks and Caicos Islands",
|
|
||||||
},
|
|
||||||
{ code: "TD", label: "Chad" },
|
|
||||||
{
|
|
||||||
code: "TF",
|
|
||||||
label: "French Southern Territories",
|
|
||||||
},
|
|
||||||
{ code: "TG", label: "Togo" },
|
|
||||||
{ code: "TH", label: "Thailand" },
|
|
||||||
{ code: "TJ", label: "Tajikistan" },
|
|
||||||
{ code: "TK", label: "Tokelau" },
|
|
||||||
{ code: "TL", label: "Timor-Leste" },
|
|
||||||
{ code: "TM", label: "Turkmenistan" },
|
|
||||||
{ code: "TN", label: "Tunisia" },
|
|
||||||
{ code: "TO", label: "Tonga" },
|
|
||||||
{ code: "TR", label: "Turkey" },
|
|
||||||
{
|
|
||||||
code: "TT",
|
|
||||||
label: "Trinidad and Tobago",
|
|
||||||
},
|
|
||||||
{ code: "TV", label: "Tuvalu" },
|
|
||||||
{
|
|
||||||
code: "TW",
|
|
||||||
label: "Taiwan, Province of China",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
code: "TZ",
|
|
||||||
label: "United Republic of Tanzania",
|
|
||||||
},
|
|
||||||
{ code: "UA", label: "Ukraine" },
|
|
||||||
{ code: "UG", label: "Uganda" },
|
|
||||||
{
|
|
||||||
code: "US",
|
|
||||||
label: "United States",
|
|
||||||
},
|
|
||||||
{ code: "UY", label: "Uruguay" },
|
|
||||||
{ code: "UZ", label: "Uzbekistan" },
|
|
||||||
{
|
|
||||||
code: "VA",
|
|
||||||
label: "Holy See (Vatican City State)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
code: "VC",
|
|
||||||
label: "Saint Vincent and the Grenadines",
|
|
||||||
},
|
|
||||||
{ code: "VE", label: "Venezuela" },
|
|
||||||
{
|
|
||||||
code: "VG",
|
|
||||||
label: "British Virgin Islands",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
code: "VI",
|
|
||||||
label: "US Virgin Islands",
|
|
||||||
},
|
|
||||||
{ code: "VN", label: "Vietnam" },
|
|
||||||
{ code: "VU", label: "Vanuatu" },
|
|
||||||
{ code: "WF", label: "Wallis and Futuna" },
|
|
||||||
{ code: "WS", label: "Samoa" },
|
|
||||||
{ code: "XK", label: "Kosovo" },
|
|
||||||
{ code: "YE", label: "Yemen" },
|
|
||||||
{ code: "YT", label: "Mayotte" },
|
|
||||||
{ code: "ZA", label: "South Africa" },
|
|
||||||
{ code: "ZM", label: "Zambia" },
|
|
||||||
{ code: "ZW", label: "Zimbabwe" },
|
|
||||||
];
|
|
|
@ -1,53 +0,0 @@
|
||||||
import { TextField } from "@material-ui/core";
|
|
||||||
import { Autocomplete } from "@material-ui/lab";
|
|
||||||
import { makeStyles } from "@saleor/macaw-ui";
|
|
||||||
import { ControllerRenderProps } from "react-hook-form";
|
|
||||||
import { ChannelConfig } from "../../channels-configuration/channels-config";
|
|
||||||
import { countries } from "./countries";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
type CountrySelectProps = ControllerRenderProps<ChannelConfig, "address.country">;
|
|
||||||
|
|
||||||
// TODO: replace with macaw-ui component
|
|
||||||
const useStyles = makeStyles({
|
|
||||||
root: {
|
|
||||||
padding: "7px 9px !important",
|
|
||||||
},
|
|
||||||
clearIndicator: {
|
|
||||||
marginRight: 2,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// eslint-disable-next-line react/display-name
|
|
||||||
export const CountrySelect = React.forwardRef((p: CountrySelectProps, ref) => {
|
|
||||||
const styles = useStyles();
|
|
||||||
const { onChange, value } = p;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Autocomplete
|
|
||||||
classes={{
|
|
||||||
inputRoot: styles.root,
|
|
||||||
clearIndicator: styles.clearIndicator,
|
|
||||||
}}
|
|
||||||
options={countries}
|
|
||||||
onChange={(_, data) => onChange(data ? data.code : null)}
|
|
||||||
value={
|
|
||||||
countries.find((country) => {
|
|
||||||
return value === country.code;
|
|
||||||
}) ?? null
|
|
||||||
}
|
|
||||||
getOptionLabel={(option) => option.label}
|
|
||||||
renderInput={(params) => (
|
|
||||||
<TextField
|
|
||||||
{...params}
|
|
||||||
inputRef={ref}
|
|
||||||
placeholder={"Country"}
|
|
||||||
inputProps={{
|
|
||||||
...params.inputProps,
|
|
||||||
autoComplete: "new-password", // disable autocomplete and autofill
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
|
@ -1,52 +0,0 @@
|
||||||
import { Typography } from "@material-ui/core";
|
|
||||||
import { makeStyles } from "@saleor/macaw-ui";
|
|
||||||
import { AppLink } from "./app-link";
|
|
||||||
import { AppPaper } from "./app-paper";
|
|
||||||
import { AppTabNavButton } from "./app-tab-nav-button";
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
|
||||||
root: {
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: "16px",
|
|
||||||
},
|
|
||||||
list: {
|
|
||||||
paddingLeft: "16px",
|
|
||||||
margin: 0,
|
|
||||||
color: "inherit",
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const Instructions = () => {
|
|
||||||
const styles = useStyles();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AppPaper>
|
|
||||||
<div className={styles.root}>
|
|
||||||
<Typography variant="h4">Use external service for tax calculation</Typography>
|
|
||||||
<Typography variant="body1">
|
|
||||||
<ol className={styles.list}>
|
|
||||||
<li>
|
|
||||||
Go to <AppLink href="/taxes/channels">Tax Configuration</AppLink>. Make sure you chose{" "}
|
|
||||||
<q>Use tax app</q> as the method of tax calculation for your channel.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
In the Tax App, go to the <AppTabNavButton to="providers">Providers</AppTabNavButton>{" "}
|
|
||||||
tab to add an instance of your provider. Click <q>Add provider</q>, and select the tax
|
|
||||||
provider you want to use. Fill in the configuration form and hit <q>Save</q>.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Go to the <AppTabNavButton to="channels">Channels</AppTabNavButton> tab. Select a
|
|
||||||
channel. In the <q>Channel tax provider</q> field, select the created instance. Fill
|
|
||||||
in the rest of the form, and hit <q>Save</q>.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Saleor will now use the channel's configured tax provider for order & checkout tax
|
|
||||||
calculations.
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
</AppPaper>
|
|
||||||
);
|
|
||||||
};
|
|
93
apps/taxes/src/modules/ui/providers.tsx
Normal file
93
apps/taxes/src/modules/ui/providers.tsx
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
import { Box, BoxProps, Button, Text } from "@saleor/macaw-ui/next";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { trpcClient } from "../trpc/trpc-client";
|
||||||
|
import { AppCard } from "./app-card";
|
||||||
|
|
||||||
|
const AddProvider = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
display={"flex"}
|
||||||
|
flexDirection={"column"}
|
||||||
|
gap={6}
|
||||||
|
alignItems={"center"}
|
||||||
|
height={"100%"}
|
||||||
|
justifyContent={"center"}
|
||||||
|
>
|
||||||
|
<Text variant="body" __fontWeight={"400"}>
|
||||||
|
No providers configured yet
|
||||||
|
</Text>
|
||||||
|
<Button onClick={() => router.push("/providers")}>Add first provider</Button>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Skeleton = () => {
|
||||||
|
// todo: replace with skeleton
|
||||||
|
return (
|
||||||
|
<Box height={"100%"} display={"flex"} alignItems={"center"} justifyContent={"center"}>
|
||||||
|
Loading...
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Table = {
|
||||||
|
Container: (props: BoxProps) => <Box __textAlign={"left"} width="100%" {...props} as="table" />,
|
||||||
|
THead: (props: BoxProps) => <Box {...props} as="thead" />,
|
||||||
|
TR: (props: BoxProps) => <Box {...props} as="tr" />,
|
||||||
|
TH: (props: BoxProps) => (
|
||||||
|
<Box fontWeight={"captionSmall"} fontSize={"captionSmall"} {...props} as="th" />
|
||||||
|
),
|
||||||
|
TBody: (props: BoxProps) => <Box {...props} as="tbody" />,
|
||||||
|
TD: (props: BoxProps) => <Box fontSize="bodyMedium" {...props} as="td" />,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProvidersTable = () => {
|
||||||
|
const { data } = trpcClient.providersConfiguration.getAll.useQuery();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table.Container>
|
||||||
|
<Table.THead color={"textNeutralSubdued"}>
|
||||||
|
<Table.TR>
|
||||||
|
<Table.TH>Name</Table.TH>
|
||||||
|
<Table.TH>Provider</Table.TH>
|
||||||
|
<Table.TH>Status</Table.TH>
|
||||||
|
</Table.TR>
|
||||||
|
</Table.THead>
|
||||||
|
<Table.TBody>
|
||||||
|
{data?.map((item) => (
|
||||||
|
<Table.TR>
|
||||||
|
<Table.TD>{item.config.name}</Table.TD>
|
||||||
|
<Table.TD>{item.provider}</Table.TD>
|
||||||
|
<Table.TD>{"Status"}</Table.TD>
|
||||||
|
</Table.TR>
|
||||||
|
))}
|
||||||
|
</Table.TBody>
|
||||||
|
</Table.Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Providers = () => {
|
||||||
|
const { data, isFetching, isFetched } = trpcClient.providersConfiguration.getAll.useQuery();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const isProvider = (data?.length ?? 0) > 0;
|
||||||
|
const isResult = isFetched && isProvider;
|
||||||
|
const isNoResult = isFetched && !isProvider;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppCard __minHeight={"320px"} height="100%">
|
||||||
|
{isFetching && <Skeleton />}
|
||||||
|
{isNoResult && <AddProvider />}
|
||||||
|
{isResult && (
|
||||||
|
<>
|
||||||
|
<ProvidersTable />
|
||||||
|
<Box>
|
||||||
|
<Button onClick={() => router.push("/providers")}>Add new</Button>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AppCard>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,27 +1,16 @@
|
||||||
import "../styles/globals.css";
|
import "../styles/globals.css";
|
||||||
|
import "@saleor/macaw-ui/next/style";
|
||||||
import { Theme } from "@material-ui/core/styles";
|
|
||||||
import { AppBridge, AppBridgeProvider } from "@saleor/app-sdk/app-bridge";
|
import { AppBridge, AppBridgeProvider } from "@saleor/app-sdk/app-bridge";
|
||||||
import { RoutePropagator } from "@saleor/app-sdk/app-bridge/next";
|
import { RoutePropagator } from "@saleor/app-sdk/app-bridge/next";
|
||||||
import {
|
import { ThemeProvider } from "@saleor/macaw-ui/next";
|
||||||
dark,
|
|
||||||
light,
|
import { AppProps } from "next/app";
|
||||||
SaleorThemeColors,
|
import { useEffect } from "react";
|
||||||
ThemeProvider as MacawUIThemeProvider,
|
|
||||||
} from "@saleor/macaw-ui";
|
|
||||||
import React, { PropsWithChildren, useEffect } from "react";
|
|
||||||
import { AppProps } from "next/app";
|
|
||||||
|
|
||||||
import { GraphQLProvider } from "../providers/GraphQLProvider";
|
|
||||||
import { ThemeSynchronizer } from "../lib/theme-synchronizer";
|
import { ThemeSynchronizer } from "../lib/theme-synchronizer";
|
||||||
import { trpcClient } from "../modules/trpc/trpc-client";
|
import { trpcClient } from "../modules/trpc/trpc-client";
|
||||||
import { NoSSRWrapper } from "../lib/no-ssr-wrapper";
|
import { GraphQLProvider } from "../providers/GraphQLProvider";
|
||||||
|
import { NoSSRWrapper } from "@saleor/apps-shared";
|
||||||
const themeOverrides: Partial<Theme> = {
|
|
||||||
/**
|
|
||||||
* You can override MacawUI theme here
|
|
||||||
*/
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure instance is a singleton.
|
* Ensure instance is a singleton.
|
||||||
|
@ -29,36 +18,6 @@ const themeOverrides: Partial<Theme> = {
|
||||||
*/
|
*/
|
||||||
export const appBridgeInstance = typeof window !== "undefined" ? new AppBridge() : undefined;
|
export const appBridgeInstance = typeof window !== "undefined" ? new AppBridge() : undefined;
|
||||||
|
|
||||||
type PalettesOverride = Record<"light" | "dark", SaleorThemeColors>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Temporary override of colors, to match new dashboard palette.
|
|
||||||
* Long term this will be replaced with Macaw UI 2.x with up to date design tokens
|
|
||||||
*/
|
|
||||||
const palettes: PalettesOverride = {
|
|
||||||
light: {
|
|
||||||
...light,
|
|
||||||
background: {
|
|
||||||
default: "#fff",
|
|
||||||
paper: "#fff",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dark: {
|
|
||||||
...dark,
|
|
||||||
background: {
|
|
||||||
default: "hsla(211, 42%, 14%, 1)",
|
|
||||||
paper: "hsla(211, 42%, 14%, 1)",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* That's a hack required by Macaw-UI incompatibility with React@18
|
|
||||||
*/
|
|
||||||
const ThemeProvider = MacawUIThemeProvider as React.FC<
|
|
||||||
PropsWithChildren<{ overrides?: Partial<Theme>; ssr: boolean; palettes: PalettesOverride }>
|
|
||||||
>;
|
|
||||||
|
|
||||||
function NextApp({ Component, pageProps }: AppProps) {
|
function NextApp({ Component, pageProps }: AppProps) {
|
||||||
/**
|
/**
|
||||||
* Configure JSS (used by MacawUI) for SSR. If Macaw is not used, can be removed.
|
* Configure JSS (used by MacawUI) for SSR. If Macaw is not used, can be removed.
|
||||||
|
@ -75,7 +34,7 @@ function NextApp({ Component, pageProps }: AppProps) {
|
||||||
<NoSSRWrapper>
|
<NoSSRWrapper>
|
||||||
<AppBridgeProvider appBridgeInstance={appBridgeInstance}>
|
<AppBridgeProvider appBridgeInstance={appBridgeInstance}>
|
||||||
<GraphQLProvider>
|
<GraphQLProvider>
|
||||||
<ThemeProvider palettes={palettes} overrides={themeOverrides} ssr>
|
<ThemeProvider>
|
||||||
<ThemeSynchronizer />
|
<ThemeSynchronizer />
|
||||||
<RoutePropagator />
|
<RoutePropagator />
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
|
@ -85,5 +44,3 @@ function NextApp({ Component, pageProps }: AppProps) {
|
||||||
</NoSSRWrapper>
|
</NoSSRWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default trpcClient.withTRPC(NextApp);
|
|
||||||
|
|
|
@ -1,69 +1,39 @@
|
||||||
import { makeStyles, PageTab, PageTabs } from "@saleor/macaw-ui";
|
import { Box, Text } from "@saleor/macaw-ui/next";
|
||||||
import { ChannelTaxProvider } from "../modules/channels/ui/channel-tax-provider";
|
import { AppColumns } from "../modules/ui/app-columns";
|
||||||
import { Channels } from "../modules/channels/ui/channels";
|
import { Providers } from "../modules/ui/providers";
|
||||||
import { Configuration } from "../modules/providers-configuration/ui/configuration";
|
|
||||||
import { ProvidersInstances } from "../modules/providers-configuration/ui/providers-instances";
|
|
||||||
import { useActiveTab } from "../modules/taxes/tax-context";
|
|
||||||
import { AppContainer } from "../modules/ui/app-container";
|
|
||||||
import { AppLayout } from "../modules/ui/app-layout";
|
|
||||||
import { Instructions } from "../modules/ui/instructions";
|
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
const Header = () => {
|
||||||
tabs: {
|
|
||||||
margin: "16px 0",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const ChannelTab = () => {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Box>
|
||||||
<Channels />
|
<Text as="p" variant="body" __fontWeight={"400"}>
|
||||||
<ChannelTaxProvider />
|
Please configure the app by connecting one of the supported tax providers.
|
||||||
</>
|
</Text>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ProvidersTab = () => {
|
const Intro = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<Box display="flex" flexDirection={"column"} gap={10}>
|
||||||
<ProvidersInstances />
|
<Text as="h2" variant="heading">
|
||||||
<Configuration />
|
Tax providers
|
||||||
</>
|
</Text>
|
||||||
|
<Text as="p" variant="body" __fontWeight={"400"}>
|
||||||
|
Manage providers configuration to connect Saleor with providers.
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const tabs = {
|
|
||||||
channels: {
|
|
||||||
component: <ChannelTab />,
|
|
||||||
label: "Channels",
|
|
||||||
},
|
|
||||||
providers: {
|
|
||||||
component: <ProvidersTab />,
|
|
||||||
label: "Providers",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AppTab = keyof typeof tabs;
|
|
||||||
|
|
||||||
const ConfigurationPage = () => {
|
const ConfigurationPage = () => {
|
||||||
const styles = useStyles();
|
|
||||||
const { activeTab, setActiveTab } = useActiveTab();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<AppContainer>
|
<AppColumns
|
||||||
<div className={styles.tabs}>
|
gridRatio="1/2"
|
||||||
<PageTabs value={activeTab} onChange={(value) => setActiveTab(value as AppTab)}>
|
top={<Header />}
|
||||||
{Object.entries(tabs).map(([key, config]) => (
|
bottomLeft={<Intro />}
|
||||||
<PageTab key={key} value={key} label={config.label} />
|
bottomRight={<Providers />}
|
||||||
))}
|
/>
|
||||||
</PageTabs>
|
|
||||||
</div>
|
|
||||||
</AppContainer>
|
|
||||||
<AppLayout>
|
|
||||||
{tabs[activeTab].component}
|
|
||||||
<Instructions />
|
|
||||||
</AppLayout>
|
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { useAppBridge } from "@saleor/app-sdk/app-bridge";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useIsMounted } from "usehooks-ts";
|
import { useIsMounted } from "usehooks-ts";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { LinearProgress } from "@material-ui/core";
|
|
||||||
import { isInIframe } from "@saleor/apps-shared";
|
import { isInIframe } from "@saleor/apps-shared";
|
||||||
|
|
||||||
const IndexPage: NextPage = () => {
|
const IndexPage: NextPage = () => {
|
||||||
|
@ -18,7 +17,7 @@ const IndexPage: NextPage = () => {
|
||||||
}, [isMounted, appBridgeState?.ready, replace]);
|
}, [isMounted, appBridgeState?.ready, replace]);
|
||||||
|
|
||||||
if (isInIframe()) {
|
if (isInIframe()) {
|
||||||
return <LinearProgress />;
|
return <span>Loading...</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
11
apps/taxes/src/pages/providers/avatax.tsx
Normal file
11
apps/taxes/src/pages/providers/avatax.tsx
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* * placeholder
|
||||||
|
* // todo: add new avatax config view
|
||||||
|
*/
|
||||||
|
const NewAvataxPage = () => {
|
||||||
|
return <main>Avatax</main>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NewAvataxPage;
|
128
apps/taxes/src/pages/providers/index.tsx
Normal file
128
apps/taxes/src/pages/providers/index.tsx
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
import { Box, Button, Text } from "@saleor/macaw-ui/next";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { AvataxIcon, StripeTaxIcon, TaxJarIcon } from "../../assets";
|
||||||
|
import { AppCard } from "../../modules/ui/app-card";
|
||||||
|
import { AppColumns } from "../../modules/ui/app-columns";
|
||||||
|
|
||||||
|
const Header = () => {
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Text __maxWidth={"360px"} __fontWeight={"400"} variant="body">
|
||||||
|
Select and configure providers to connect Saleor with selected services.
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Intro = () => {
|
||||||
|
return (
|
||||||
|
<Box gap={6} display="flex" flexDirection={"column"}>
|
||||||
|
<Text variant="heading" as="h3">
|
||||||
|
Choose provider
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type ProviderProps = {
|
||||||
|
label: string;
|
||||||
|
icon: string;
|
||||||
|
description: React.ReactNode;
|
||||||
|
isComingSoon?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const providerConfig = {
|
||||||
|
taxjar: {
|
||||||
|
label: "TaxJar",
|
||||||
|
icon: TaxJarIcon,
|
||||||
|
description: (
|
||||||
|
<p>
|
||||||
|
TaxJar is a cloud-based tax automation platform designed to simplify and streamline sales
|
||||||
|
tax management for online sellers.
|
||||||
|
</p>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
avatax: {
|
||||||
|
label: "Avatax",
|
||||||
|
icon: AvataxIcon,
|
||||||
|
description: (
|
||||||
|
<p>
|
||||||
|
Avatax is a comprehensive tax automation software service that helps businesses calculate
|
||||||
|
and manage sales tax accurately and efficiently.
|
||||||
|
</p>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
stripeTax: {
|
||||||
|
label: "Stripe Tax",
|
||||||
|
icon: StripeTaxIcon,
|
||||||
|
isComingSoon: true,
|
||||||
|
description: (
|
||||||
|
<p>
|
||||||
|
Stripe Tax lets you calculate, collect, and report tax on global payments with a single
|
||||||
|
integration.
|
||||||
|
</p>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
} satisfies Record<string, ProviderProps>;
|
||||||
|
|
||||||
|
const ProviderCard = ({
|
||||||
|
label,
|
||||||
|
icon,
|
||||||
|
description,
|
||||||
|
provider,
|
||||||
|
isComingSoon,
|
||||||
|
}: ProviderProps & { provider: string }) => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppCard>
|
||||||
|
<Box display={"flex"} flexDirection={"column"} gap={8}>
|
||||||
|
<Box display={"flex"} justifyContent={"space-between"}>
|
||||||
|
<Box alignItems={"center"} display={"flex"} gap={6}>
|
||||||
|
<Image src={icon} width={20} height={20} alt={`provider icon`} />
|
||||||
|
<Text variant="bodyStrong">{label}</Text>
|
||||||
|
</Box>
|
||||||
|
{isComingSoon && (
|
||||||
|
<Text
|
||||||
|
variant="body"
|
||||||
|
fontSize={"headingSmall"}
|
||||||
|
color={"textNeutralSubdued"}
|
||||||
|
textTransform={"uppercase"}
|
||||||
|
>
|
||||||
|
Coming soon
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<Text __fontWeight={"400"} variant="body" __maxWidth={"480px"}>
|
||||||
|
{description}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Box display={"flex"} justifyContent={"flex-end"} marginTop={12}>
|
||||||
|
{!isComingSoon && (
|
||||||
|
<Button onClick={() => router.push(`/providers/${provider}`)}>Choose</Button>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</AppCard>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ChooseProvider = () => {
|
||||||
|
return (
|
||||||
|
<Box gap={6} display="flex" flexDirection={"column"}>
|
||||||
|
{Object.entries(providerConfig).map(([provider, description]) => {
|
||||||
|
return <ProviderCard {...description} provider={provider} />;
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const NewProviderPage = () => {
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<AppColumns top={<Header />} bottomLeft={<Intro />} bottomRight={<ChooseProvider />} />
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NewProviderPage;
|
11
apps/taxes/src/pages/providers/taxjar.tsx
Normal file
11
apps/taxes/src/pages/providers/taxjar.tsx
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* * placeholder
|
||||||
|
* // todo: add new taxjar config view
|
||||||
|
*/
|
||||||
|
const NewTaxJarPage = () => {
|
||||||
|
return <main>TaxJar</main>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NewTaxJarPage;
|
|
@ -1,5 +1,6 @@
|
||||||
.link {
|
.link {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.link:hover {
|
.link:hover {
|
||||||
|
|
7534
pnpm-lock.yaml
7534
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue