feat: taxes client logs (#944)

* feat: 🚧 add skeleton logs page

* feat:  add ClientLogsMetadataRepository

* feat:  add AvataxClientLogger and router

* feat:  implement avatax client logs in order confirmed

* feat: 🚧 add skeleton ui for logs

* feat:  add avatax client logger

* refactor: ♻️ move breadcrumbs to page lvl

* feat:  logger per config

* feat:  add logs to remaining events

* refactor: ♻️ pass clientLogger from webhook service

* feat:  add taxjar logger

* test:  add tests for logs push

* feat:  add getAll test

* feat:  add missing taxjar page

* refactor: ♻️ move unshiftItemToLimitedArray to log-utils and test

* fix: 🐛 label

* refactor: 💄 styles

* build: 👷 changeset

* fix: 🐛 typo

* refactor: ♻️ address feedback on backend side

* refactor: ♻️ adjust UI to merging of avatax & taxjar logs

* feat:  make client logs table box scrollable

* fix: 🐛 size of container

* fix: 🐛 loading on isRefetching, not isFetching

* fix: 🐛 no lines early error
This commit is contained in:
Adrian Pilarczyk 2023-09-07 10:42:54 +02:00 committed by GitHub
parent 4a2275e999
commit a32fe7caf4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 1264 additions and 261 deletions

View file

@ -0,0 +1,5 @@
---
"saleor-app-taxes": minor
---
Added logs for AvaTax and TaxJar. Logs are stored in the app metadata. Only the last 100 events are stored. Each provider configuration has its own logs. You can get to them by a new button "Logs" in the provider table.

View file

@ -1,35 +1,44 @@
import { AuthData } from "@saleor/app-sdk/APL";
import { OrderConfirmedSubscriptionFragment, TaxBaseFragment } from "../../../generated/graphql";
import { OrderConfirmedSubscriptionFragment } from "../../../generated/graphql";
import { Logger, createLogger } from "../../lib/logger";
import { CalculateTaxesPayload } from "../../pages/api/webhooks/checkout-calculate-taxes";
import { OrderCancelledPayload } from "../../pages/api/webhooks/order-cancelled";
import { ProviderWebhookService } from "../taxes/tax-provider-webhook";
import { AvataxClient } from "./avatax-client";
import { AvataxConfig, defaultAvataxConfig } from "./avatax-connection-schema";
import { AvataxConfig } from "./avatax-connection-schema";
import { AvataxCalculateTaxesAdapter } from "./calculate-taxes/avatax-calculate-taxes-adapter";
import { ClientLogger, createClientLogger } from "../logs/client-logger";
import { AvataxOrderCancelledAdapter } from "./order-cancelled/avatax-order-cancelled-adapter";
import { AvataxOrderConfirmedAdapter } from "./order-confirmed/avatax-order-confirmed-adapter";
import { CalculateTaxesPayload } from "../../pages/api/webhooks/checkout-calculate-taxes";
export class AvataxWebhookService implements ProviderWebhookService {
config = defaultAvataxConfig;
client: AvataxClient;
private logger: Logger;
private config: AvataxConfig;
private clientLogger: ClientLogger;
private authData: AuthData;
constructor(
config: AvataxConfig,
private authData: AuthData,
) {
constructor({
config,
authData,
clientLogger,
}: {
config: AvataxConfig;
authData: AuthData;
clientLogger: ClientLogger;
}) {
this.logger = createLogger({
name: "AvataxWebhookService",
});
const avataxClient = new AvataxClient(config);
this.authData = authData;
this.config = config;
this.client = avataxClient;
this.clientLogger = clientLogger;
}
async calculateTaxes(payload: CalculateTaxesPayload) {
const adapter = new AvataxCalculateTaxesAdapter(this.config, this.authData);
const adapter = new AvataxCalculateTaxesAdapter({
config: this.config,
clientLogger: this.clientLogger,
authData: this.authData,
});
const response = await adapter.send(payload);
@ -37,7 +46,11 @@ export class AvataxWebhookService implements ProviderWebhookService {
}
async confirmOrder(order: OrderConfirmedSubscriptionFragment) {
const adapter = new AvataxOrderConfirmedAdapter(this.config, this.authData);
const adapter = new AvataxOrderConfirmedAdapter({
config: this.config,
clientLogger: this.clientLogger,
authData: this.authData,
});
const response = await adapter.send({ order });
@ -45,7 +58,10 @@ export class AvataxWebhookService implements ProviderWebhookService {
}
async cancelOrder(payload: OrderCancelledPayload) {
const adapter = new AvataxOrderCancelledAdapter(this.config);
const adapter = new AvataxOrderCancelledAdapter({
config: this.config,
clientLogger: this.clientLogger,
});
await adapter.send(payload);
}

View file

@ -5,6 +5,7 @@ import { CalculateTaxesResponse } from "../../taxes/tax-provider-webhook";
import { WebhookAdapter } from "../../taxes/tax-webhook-adapter";
import { AvataxClient, CreateTransactionArgs } from "../avatax-client";
import { AvataxConfig } from "../avatax-connection-schema";
import { ClientLogger } from "../../logs/client-logger";
import { AvataxCalculateTaxesPayloadService } from "./avatax-calculate-taxes-payload.service";
import { AvataxCalculateTaxesResponseTransformer } from "./avatax-calculate-taxes-response-transformer";
@ -17,33 +18,64 @@ export class AvataxCalculateTaxesAdapter
implements WebhookAdapter<CalculateTaxesPayload, AvataxCalculateTaxesResponse>
{
private logger: Logger;
constructor(
private readonly config: AvataxConfig,
private authData: AuthData,
) {
private readonly config: AvataxConfig;
private readonly authData: AuthData;
private readonly clientLogger: ClientLogger;
constructor({
config,
authData,
clientLogger,
}: {
config: AvataxConfig;
clientLogger: ClientLogger;
authData: AuthData;
}) {
this.logger = createLogger({ name: "AvataxCalculateTaxesAdapter" });
this.config = config;
this.clientLogger = clientLogger;
this.authData = authData;
}
async send(payload: CalculateTaxesPayload): Promise<AvataxCalculateTaxesResponse> {
this.logger.debug(
{ payload },
"Transforming the Saleor payload for calculating taxes with AvaTax...",
);
this.logger.debug("Transforming the Saleor payload for calculating taxes with AvaTax...");
const payloadService = new AvataxCalculateTaxesPayloadService(this.authData);
const target = await payloadService.getPayload(payload, this.config);
this.logger.debug("Calling AvaTax createTransaction with transformed payload...");
const client = new AvataxClient(this.config);
const response = await client.createTransaction(target);
this.logger.debug("AvaTax createTransaction successfully responded");
try {
const response = await client.createTransaction(target);
const responseTransformer = new AvataxCalculateTaxesResponseTransformer();
const transformedResponse = responseTransformer.transform(response);
this.clientLogger.push({
event: "[CalculateTaxes] createTransaction",
status: "success",
payload: {
input: target,
output: response,
},
});
this.logger.debug("Transformed AvaTax createTransaction response");
this.logger.debug("AvaTax createTransaction successfully responded");
return transformedResponse;
const responseTransformer = new AvataxCalculateTaxesResponseTransformer();
const transformedResponse = responseTransformer.transform(response);
this.logger.debug("Transformed AvaTax createTransaction response");
return transformedResponse;
} catch (error) {
this.clientLogger.push({
event: "[CalculateTaxes] createTransaction",
status: "error",
payload: {
input: target,
output: error,
},
});
throw error;
}
}
}

View file

@ -3,15 +3,20 @@ import { OrderCancelledPayload } from "../../../pages/api/webhooks/order-cancell
import { WebhookAdapter } from "../../taxes/tax-webhook-adapter";
import { AvataxClient, VoidTransactionArgs } from "../avatax-client";
import { AvataxConfig } from "../avatax-connection-schema";
import { ClientLogger } from "../../logs/client-logger";
import { AvataxOrderCancelledPayloadTransformer } from "./avatax-order-cancelled-payload-transformer";
export type AvataxOrderCancelledTarget = VoidTransactionArgs;
export class AvataxOrderCancelledAdapter implements WebhookAdapter<OrderCancelledPayload, void> {
private logger: Logger;
private readonly clientLogger: ClientLogger;
private readonly config: AvataxConfig;
constructor(private readonly config: AvataxConfig) {
constructor({ config, clientLogger }: { config: AvataxConfig; clientLogger: ClientLogger }) {
this.logger = createLogger({ name: "AvataxOrderCancelledAdapter" });
this.config = config;
this.clientLogger = clientLogger;
}
async send(payload: OrderCancelledPayload) {
@ -24,8 +29,28 @@ export class AvataxOrderCancelledAdapter implements WebhookAdapter<OrderCancelle
const client = new AvataxClient(this.config);
await client.voidTransaction(target);
try {
const response = await client.voidTransaction(target);
this.logger.debug(`Successfully voided the transaction of id: ${target.transactionCode}`);
this.clientLogger.push({
event: "[OrderCancelled] voidTransaction",
status: "success",
payload: {
input: target,
output: response,
},
});
this.logger.debug(`Successfully voided the transaction of id: ${target.transactionCode}`);
} catch (error) {
this.clientLogger.push({
event: "[OrderCancelled] voidTransaction",
status: "error",
payload: {
input: target,
output: error,
},
});
}
}
}

View file

@ -5,6 +5,7 @@ import { CreateOrderResponse } from "../../taxes/tax-provider-webhook";
import { WebhookAdapter } from "../../taxes/tax-webhook-adapter";
import { AvataxClient } from "../avatax-client";
import { AvataxConfig } from "../avatax-connection-schema";
import { ClientLogger } from "../../logs/client-logger";
import { AvataxOrderConfirmedPayloadService } from "./avatax-order-confirmed-payload.service";
import { AvataxOrderConfirmedResponseTransformer } from "./avatax-order-confirmed-response-transformer";
@ -17,9 +18,23 @@ export class AvataxOrderConfirmedAdapter
implements WebhookAdapter<AvataxOrderConfirmedPayload, AvataxOrderConfirmedResponse>
{
private logger: Logger;
private readonly config: AvataxConfig;
private readonly authData: AuthData;
private readonly clientLogger: ClientLogger;
constructor(private readonly config: AvataxConfig, private authData: AuthData) {
constructor({
config,
authData,
clientLogger,
}: {
config: AvataxConfig;
clientLogger: ClientLogger;
authData: AuthData;
}) {
this.logger = createLogger({ name: "AvataxOrderConfirmedAdapter" });
this.config = config;
this.authData = authData;
this.clientLogger = clientLogger;
}
async send(payload: AvataxOrderConfirmedPayload): Promise<AvataxOrderConfirmedResponse> {
@ -31,15 +46,37 @@ export class AvataxOrderConfirmedAdapter
this.logger.debug("Calling AvaTax createTransaction with transformed payload...");
const client = new AvataxClient(this.config);
const response = await client.createTransaction(target);
this.logger.debug("AvaTax createTransaction successfully responded");
try {
const response = await client.createTransaction(target);
const responseTransformer = new AvataxOrderConfirmedResponseTransformer();
const transformedResponse = responseTransformer.transform(response);
this.clientLogger.push({
event: "[OrderConfirmed] createTransaction",
status: "success",
payload: {
input: target,
output: response,
},
});
this.logger.debug("Transformed AvaTax createTransaction response");
this.logger.debug("AvaTax createTransaction successfully responded");
return transformedResponse;
const responseTransformer = new AvataxOrderConfirmedResponseTransformer();
const transformedResponse = responseTransformer.transform(response);
this.logger.debug("Transformed AvaTax createTransaction response");
return transformedResponse;
} catch (error) {
this.clientLogger.push({
event: "[OrderConfirmed] createTransaction",
status: "error",
payload: {
input: target,
output: error,
},
});
throw error;
}
}
}

View file

@ -0,0 +1,57 @@
import { createSettingsManager } from "../app/metadata-manager";
import { protectedClientProcedure } from "../trpc/protected-client-procedure";
import { ClientLogger, clientLogInputSchema } from "./client-logger";
import { z } from "zod";
import { router } from "../trpc/trpc-server";
import { createLogger } from "../../lib/logger";
const logProcedure = protectedClientProcedure.use(({ ctx, next }) => {
const settingsManager = createSettingsManager(ctx.apiClient, ctx.appId!);
return next({
ctx: {
settingsManager,
},
});
});
const logIdSchema = z.object({
id: z.string(),
});
const pushClientLogInputSchema = z
.object({
log: clientLogInputSchema,
})
.merge(logIdSchema);
export const clientLoggerRouter = router({
push: logProcedure.input(pushClientLogInputSchema).query(async ({ ctx, input }) => {
const logger = createLogger({
name: "ClientLoggerRouter.push",
});
logger.debug("Pushing log to metadata");
const loggerRepository = new ClientLogger({
settingsManager: ctx.settingsManager,
configurationId: input.id,
});
return loggerRepository.push(input.log);
}),
getAll: logProcedure.input(logIdSchema).query(async ({ ctx, input }) => {
const logger = createLogger({
name: "ClientLoggerRouter.getAll",
});
logger.debug("Getting logs from metadata");
const loggerRepository = new ClientLogger({
settingsManager: ctx.settingsManager,
configurationId: input.id,
});
return loggerRepository.getAll();
}),
});

View file

@ -0,0 +1,88 @@
import { AuthData } from "@saleor/app-sdk/APL";
import { createGraphQLClient } from "@saleor/apps-shared";
import { z } from "zod";
import { createSettingsManager } from "../app/metadata-manager";
import {
ClientLogsMetadataRepository,
ClientLogsMetadataRepositoryParams,
MetadataLogs,
} from "./client-logs-metadata-repository";
const clientLogSchema = z.object({
date: z.string(),
event: z.string(),
status: z.enum(["success", "error"]),
payload: z.string().optional(),
});
export const clientLogInputSchema = clientLogSchema.pick({ event: true, status: true }).merge(
z.object({
payload: z
.object({
input: z.unknown(),
output: z.unknown(),
})
.optional(),
}),
);
type LogInput = z.infer<typeof clientLogInputSchema>;
export type ClientLog = z.infer<typeof clientLogSchema>;
export const LOG_LIMIT = 100;
export class ClientLogger implements MetadataLogs<ClientLog> {
private logRepository: ClientLogsMetadataRepository<ClientLog>;
constructor({
settingsManager,
configurationId,
}: Pick<ClientLogsMetadataRepositoryParams<ClientLog>, "settingsManager"> & {
configurationId: string;
}) {
this.logRepository = new ClientLogsMetadataRepository({
metadataKey: `logs-${configurationId}`,
schema: clientLogSchema,
settingsManager,
options: {
limit: LOG_LIMIT,
},
});
}
getAll() {
return this.logRepository.getAll();
}
push({ event, payload, status }: LogInput) {
const log: ClientLog = {
date: `${new Date().toLocaleDateString()} ${new Date().toLocaleTimeString()}`,
event,
payload: JSON.stringify(payload, null, 4),
status,
};
return this.logRepository.push(log);
}
}
export function createClientLogger({
authData,
configurationId,
}: {
authData: AuthData;
configurationId: string;
}) {
const { appId, saleorApiUrl, token } = authData;
const client = createGraphQLClient({
saleorApiUrl,
token,
});
const settingsManager = createSettingsManager(client, appId);
return new ClientLogger({
settingsManager,
configurationId,
});
}

View file

@ -0,0 +1,115 @@
import { EncryptedMetadataManager } from "@saleor/app-sdk/settings-manager";
import { vi, expect, it, describe } from "vitest";
import { z } from "zod";
import { ClientLogsMetadataRepository } from "./client-logs-metadata-repository";
const logSchema = z.object({
date: z.string(),
});
let mockSettingsManager = {
set: vi.fn(),
get: vi.fn(),
delete: vi.fn(),
} as unknown as EncryptedMetadataManager;
const logsRepository = new ClientLogsMetadataRepository({
metadataKey: `test-logs`,
schema: logSchema,
settingsManager: mockSettingsManager,
options: {
limit: 2,
},
});
describe("ClientLogsMetadataRepository", () => {
it("should push logs", async () => {
const log = {
date: "2021-09-02",
};
await logsRepository.push(log);
expect(mockSettingsManager.set).toHaveBeenCalledWith({
key: "test-logs",
value: JSON.stringify([log]),
});
});
it("should upsert logs", async () => {
const log = {
date: "2021-09-02",
};
logsRepository.push(log);
expect(mockSettingsManager.set).toHaveBeenCalledWith({
key: "test-logs",
value: JSON.stringify([log]),
});
const nextLog = {
date: "2021-09-03",
};
await logsRepository.push(nextLog);
expect(mockSettingsManager.set).toHaveBeenCalledWith({
key: "test-logs",
value: JSON.stringify([nextLog, log]),
});
});
it("should remove last log when pushed log exceeds limit", async () => {
const log = {
date: "2021-09-02",
};
logsRepository.push(log);
expect(mockSettingsManager.set).toHaveBeenCalledWith({
key: "test-logs",
value: JSON.stringify([log]),
});
const nextLog = {
date: "2021-09-03",
};
logsRepository.push(nextLog);
expect(mockSettingsManager.set).toHaveBeenCalledWith({
key: "test-logs",
value: JSON.stringify([nextLog, log]),
});
const lastLog = {
date: "2021-09-04",
};
await logsRepository.push(lastLog);
expect(mockSettingsManager.set).toHaveBeenCalledWith({
key: "test-logs",
value: JSON.stringify([lastLog, nextLog]),
});
});
it("should return all logs", async () => {
mockSettingsManager = {
set: vi.fn(),
get: vi.fn().mockReturnValueOnce(JSON.stringify([{ date: "2021-09-02" }])),
delete: vi.fn(),
} as unknown as EncryptedMetadataManager;
const logsRepository = new ClientLogsMetadataRepository({
metadataKey: `test-logs`,
schema: logSchema,
settingsManager: mockSettingsManager,
options: {
limit: 2,
},
});
const logs = await logsRepository.getAll();
expect(logs).toEqual([{ date: "2021-09-02" }]);
});
});

View file

@ -0,0 +1,98 @@
import { EncryptedMetadataManager, SettingsManager } from "@saleor/app-sdk/settings-manager";
import { ZodSchema, z } from "zod";
import { Logger, createLogger } from "../../lib/logger";
import { logUtils } from "./log-utils";
export interface MetadataLogs<TLog extends unknown> {
getAll(): Promise<TLog[]>;
push(payload: unknown): Promise<void>;
}
type ClientLogsMetadataRepositoryOptions = {
limit: number;
};
export type ClientLogsMetadataRepositoryParams<TLog extends unknown> = {
settingsManager: EncryptedMetadataManager;
schema: ZodSchema<TLog>;
metadataKey: string;
options: ClientLogsMetadataRepositoryOptions;
};
/**
* ClientLogsMetadataRepository is a generic class that can be used to store logs in the app metadata.
* The name "clientLogs" is used to distinguish it from the server logs. Client logs can appear in the app UI.
*/
export class ClientLogsMetadataRepository<TLog extends unknown> implements MetadataLogs<TLog> {
private readonly settingsManager: EncryptedMetadataManager;
private readonly schema: ZodSchema<TLog>;
private readonly metadataKey: string;
private readonly options: ClientLogsMetadataRepositoryOptions;
private readonly logger: Logger;
private logs: TLog[] = [];
constructor({
settingsManager,
schema,
metadataKey,
options,
}: ClientLogsMetadataRepositoryParams<TLog>) {
this.settingsManager = settingsManager;
this.schema = schema;
this.metadataKey = metadataKey;
this.options = options;
this.logger = createLogger({
name: "ClientLogsMetadataRepository",
});
}
async getAll() {
if (this.logs.length) {
this.logger.debug(`Returning cached logs for key ${this.metadataKey}`);
return this.logs;
}
const metadata = await this.settingsManager.get(this.metadataKey);
if (!metadata) {
this.logger.debug(`No metadata found for key ${this.metadataKey}`);
return [];
}
const parsedMetadata = JSON.parse(metadata);
const validation = z.array(this.schema).safeParse(parsedMetadata);
if (!validation.success) {
throw new Error(validation.error.message);
}
const logs = validation.data;
this.logs = logs;
this.logger.debug(`Returning logs for key ${this.metadataKey}`);
return logs;
}
async push(payload: unknown) {
const validation = this.schema.safeParse(payload);
if (!validation.success) {
throw new Error(validation.error.message);
}
const log = validation.data;
const logs = await this.getAll();
const nextLogs = logUtils.unshiftItemToLimitedArray(logs, log, this.options.limit);
this.logs = nextLogs;
this.logger.debug(`Pushing log to metadata for key ${this.metadataKey}`);
await this.settingsManager.set({
key: this.metadataKey,
value: JSON.stringify(nextLogs),
});
}
}

View file

@ -0,0 +1,38 @@
import { logUtils } from "./log-utils";
import { describe, expect, it } from "vitest";
describe("logUtils", () => {
describe("unshiftItemToLimitedArray", () => {
it("should unshift item to empty array", () => {
const array: string[] = [];
const item = "item";
const limit = 5;
const result = logUtils.unshiftItemToLimitedArray(array, item, limit);
expect(result).toEqual([item]);
});
it("should unshift item to array with less items than limit", () => {
const array = ["item1", "item2"];
const item = "item3";
const limit = 5;
const result = logUtils.unshiftItemToLimitedArray(array, item, limit);
expect(result).toEqual(["item3", "item1", "item2"]);
});
it("should unshift item to array with equal items than limit", () => {
const array = ["item1", "item2", "item3", "item4", "item5"];
const item = "item6";
const limit = 5;
const result = logUtils.unshiftItemToLimitedArray(array, item, limit);
expect(result).toEqual(["item6", "item1", "item2", "item3", "item4"]);
});
it("should throw error when initial array length is more than limit", () => {
const array = ["item1", "item2", "item3", "item4", "item5", "item6"];
const item = "item7";
const limit = 5;
expect(() => logUtils.unshiftItemToLimitedArray(array, item, limit)).toThrowError();
});
});
});

View file

@ -0,0 +1,24 @@
/**
* Pushes item to first place in the array and limits array length to limit.
* When array length is equal to limit, last item is removed.
* @param array Array to push item to.
* @param item Item to push to array.
* @param limit Maximum length of array.
*/
function unshiftItemToLimitedArray<T>(array: T[], item: T, limit: number) {
if (array.length > limit) {
throw new Error("Initial array length can't be more than limit");
}
const newArray = [item, ...array];
if (newArray.length > limit) {
newArray.pop();
}
return newArray;
}
export const logUtils = {
unshiftItemToLimitedArray,
};

View file

@ -0,0 +1,37 @@
import { Text } from "@saleor/macaw-ui/next";
import { Provider } from "jotai";
import { Section } from "../../ui/app-section";
import { LOG_LIMIT } from "../client-logger";
import { ClientLogs } from "./client-logs";
const LogsInstructions = () => {
return (
<Section.Description
description={
<>
<Text as="p" marginBottom={8}>
Taxes App records all API calls and responses. You can use this information to debug
issues with your provider.
</Text>
<Text as="p" marginBottom={8}>
Only the last {LOG_LIMIT} logs are stored.
</Text>
<Text as="p" marginBottom={8}>
The naming convention for each event is: <b>[WebhookName] API method name</b>.
</Text>
</>
}
/>
);
};
export const ClientLogsPage = () => {
return (
<>
<LogsInstructions />
<Provider>
<ClientLogs />
</Provider>
</>
);
};

View file

@ -0,0 +1,122 @@
import { SemanticChip } from "@saleor/apps-ui";
import { Accordion, Box, Divider, Text } from "@saleor/macaw-ui/next";
import { useRouter } from "next/router";
import { z } from "zod";
import { trpcClient } from "../../trpc/trpc-client";
import { Table } from "../../ui/table";
import { ClientLog } from "../client-logger";
type SemanticChipProps = Parameters<typeof SemanticChip>[0];
const chipVariantMap: Record<ClientLog["status"], SemanticChipProps["variant"]> = {
success: "success",
error: "error",
};
const StatusCell = ({ status }: { status: ClientLog["status"] }) => {
return <SemanticChip variant={chipVariantMap[status]}>{status}</SemanticChip>;
};
const LogRow = ({ log }: { log: ClientLog }) => {
return (
<>
<Text>{log.event}</Text>
<Box display={"flex"}>
<StatusCell status={log.status} />
</Box>
<Text variant="caption">{log.date}</Text>
</>
);
};
const LogAccordion = ({ log }: { log: ClientLog }) => {
if (log.payload) {
const prettyPayload = JSON.stringify(JSON.parse(log.payload), null, 2);
return (
<Accordion>
<Accordion.Item value={log.date}>
<Accordion.Trigger>
<Box
alignItems={"center"}
display={"grid"}
__gridTemplateColumns={"2fr 1fr 1fr auto"}
gap={4}
width={"100%"}
>
<LogRow log={log} />
<Box marginX={4}>
<Accordion.TriggerButton />
</Box>
</Box>
</Accordion.Trigger>
<Accordion.Content>
<Box display="grid">
<Box
padding={2}
marginLeft={4}
backgroundColor={"surfaceNeutralSubdued"}
as="pre"
overflowX="scroll"
__whiteSpace={"pre-wrap"}
>
{prettyPayload}
</Box>
</Box>
</Accordion.Content>
</Accordion.Item>
</Accordion>
);
}
return (
<Box>
<LogRow log={log} />
</Box>
);
};
export const ClientLogsTable = () => {
const router = useRouter();
const { id } = router.query;
const configurationId = z.string().parse(id ?? "");
const {
data: logs = [],
isFetched,
isLoading,
} = trpcClient.clientLogs.getAll.useQuery({
id: configurationId,
});
const isEmpty = isFetched && logs.length === 0;
return (
<Box __maxHeight={"80vh"} overflowY={"scroll"} marginTop={10}>
{logs.map((log, index, array) => {
const isLast = index === array.length - 1;
return (
<>
<LogAccordion key={log.date} log={log} />
{!isLast && <Divider marginY={4} />}
</>
);
})}
{isEmpty && (
<Box
display={"flex"}
flexDirection={"column"}
justifyContent={"center"}
alignItems={"center"}
gap={4}
__minHeight={"160px"}
>
<Text color="textNeutralSubdued" variant="bodyEmp">
No logs found for this configuration
</Text>
</Box>
)}
{isLoading && <Table.Skeleton />}
</Box>
);
};

View file

@ -0,0 +1,35 @@
import { Box, Button, Spinner, Text } from "@saleor/macaw-ui/next";
import { ClientLogsTable } from "./client-logs-table";
import { useRouter } from "next/router";
import { z } from "zod";
import { trpcClient } from "../../trpc/trpc-client";
const RefreshLogsButton = () => {
const router = useRouter();
const { id } = router.query;
const configurationId = z.string().parse(id ?? "");
const { refetch, isRefetching } = trpcClient.clientLogs.getAll.useQuery({
id: configurationId,
});
return (
<Button minWidth={16} variant="secondary" size="small" onClick={() => refetch()}>
{isRefetching ? <Spinner /> : "Refresh"}
</Button>
);
};
export const ClientLogs = () => {
return (
<section>
<Box alignItems={"center"} display={"flex"} justifyContent={"space-between"}>
<Text variant="heading" as="h2">
Logs
</Text>
<RefreshLogsButton />
</Box>
<ClientLogsTable />
</section>
);
};

View file

@ -1,18 +1,15 @@
import { AuthData } from "@saleor/app-sdk/APL";
import {
MetadataItem,
OrderConfirmedSubscriptionFragment,
TaxBaseFragment,
} from "../../../generated/graphql";
import { MetadataItem, OrderConfirmedSubscriptionFragment } from "../../../generated/graphql";
import { Logger, createLogger } from "../../lib/logger";
import { CalculateTaxesPayload } from "../../pages/api/webhooks/checkout-calculate-taxes";
import { OrderCancelledPayload } from "../../pages/api/webhooks/order-cancelled";
import { getAppConfig } from "../app/get-app-config";
import { AvataxWebhookService } from "../avatax/avatax-webhook.service";
import { ProviderConnection } from "../provider-connections/provider-connections";
import { TaxJarWebhookService } from "../taxjar/taxjar-webhook.service";
import { ProviderWebhookService } from "./tax-provider-webhook";
import { CalculateTaxesPayload } from "../../pages/api/webhooks/checkout-calculate-taxes";
import { createClientLogger } from "../logs/client-logger";
// todo: refactor to a factory
class ActiveTaxProviderService implements ProviderWebhookService {
@ -28,17 +25,29 @@ class ActiveTaxProviderService implements ProviderWebhookService {
});
const taxProviderName = providerConnection.provider;
const clientLogger = createClientLogger({
authData,
configurationId: providerConnection.id,
});
switch (taxProviderName) {
case "taxjar": {
this.logger.debug("Selecting TaxJar as tax provider");
this.client = new TaxJarWebhookService(providerConnection.config, this.authData);
this.client = new TaxJarWebhookService({
config: providerConnection.config,
authData: this.authData,
clientLogger,
});
break;
}
case "avatax": {
this.logger.debug("Selecting AvaTax as tax provider");
this.client = new AvataxWebhookService(providerConnection.config, this.authData);
this.client = new AvataxWebhookService({
config: providerConnection.config,
authData: this.authData,
clientLogger,
});
break;
}

View file

@ -7,6 +7,7 @@ import { FetchTaxForOrderArgs, TaxJarClient } from "../taxjar-client";
import { TaxJarConfig } from "../taxjar-connection-schema";
import { TaxJarCalculateTaxesPayloadService } from "./taxjar-calculate-taxes-payload-service";
import { TaxJarCalculateTaxesResponseTransformer } from "./taxjar-calculate-taxes-response-transformer";
import { ClientLogger } from "../../logs/client-logger";
export type TaxJarCalculateTaxesPayload = {
taxBase: TaxBaseFragment;
@ -19,8 +20,23 @@ export class TaxJarCalculateTaxesAdapter
implements WebhookAdapter<TaxJarCalculateTaxesPayload, TaxJarCalculateTaxesResponse>
{
private logger: Logger;
constructor(private readonly config: TaxJarConfig, private authData: AuthData) {
private authData: AuthData;
private readonly config: TaxJarConfig;
private readonly clientLogger: ClientLogger;
constructor({
config,
clientLogger,
authData,
}: {
config: TaxJarConfig;
clientLogger: ClientLogger;
authData: AuthData;
}) {
this.logger = createLogger({ name: "TaxJarCalculateTaxesAdapter" });
this.clientLogger = clientLogger;
this.authData = authData;
this.config = config;
}
// todo: refactor because its getting too big
@ -32,15 +48,37 @@ export class TaxJarCalculateTaxesAdapter
this.logger.debug("Calling TaxJar fetchTaxForOrder with transformed payload...");
const client = new TaxJarClient(this.config);
const response = await client.fetchTaxForOrder(target);
this.logger.debug("TaxJar fetchTaxForOrder responded with:");
try {
const response = await client.fetchTaxForOrder(target);
const responseTransformer = new TaxJarCalculateTaxesResponseTransformer();
const transformedResponse = responseTransformer.transform(payload, response);
this.clientLogger.push({
event: "[CalculateTaxes] fetchTaxForOrder",
status: "success",
payload: {
input: target,
output: response,
},
});
this.logger.debug("Transformed TaxJar fetchTaxForOrder response to");
this.logger.debug("TaxJar fetchTaxForOrder responded with:");
return transformedResponse;
const responseTransformer = new TaxJarCalculateTaxesResponseTransformer();
const transformedResponse = responseTransformer.transform(payload, response);
this.logger.debug("Transformed TaxJar fetchTaxForOrder response to");
return transformedResponse;
} catch (error) {
this.clientLogger.push({
event: "[CalculateTaxes] fetchTaxForOrder",
status: "error",
payload: {
input: target,
output: error,
},
});
throw error;
}
}
}

View file

@ -1,9 +1,18 @@
import { Logger, createLogger } from "../../../lib/logger";
import { taxJarAddressFactory } from "../address-factory";
import { TaxJarClient } from "../taxjar-client";
import { TaxJarConfig } from "../taxjar-connection-schema";
import { TaxJarValidationErrorResolver } from "./tax-jar-validation-error-resolver";
export class TaxJarValidationService {
private logger: Logger;
constructor() {
this.logger = createLogger({
name: "TaxJarValidationService",
});
}
async validate(config: TaxJarConfig): Promise<void> {
const taxJarClient = new TaxJarClient(config);
@ -13,6 +22,7 @@ export class TaxJarValidationService {
// if the address is invalid, TaxJar will throw an error (rather than 200 and error messages)
await taxJarClient.validateAddress({ params: address });
} catch (error) {
this.logger.debug({ error });
const errorResolver = new TaxJarValidationErrorResolver();
throw errorResolver.resolve(error);

View file

@ -7,6 +7,7 @@ import { CreateOrderArgs, TaxJarClient } from "../taxjar-client";
import { TaxJarConfig } from "../taxjar-connection-schema";
import { TaxJarOrderConfirmedPayloadService } from "./taxjar-order-confirmed-payload.service";
import { TaxJarOrderConfirmedResponseTransformer } from "./taxjar-order-confirmed-response-transformer";
import { ClientLogger } from "../../logs/client-logger";
export type TaxJarOrderConfirmedPayload = {
order: OrderConfirmedSubscriptionFragment;
@ -18,7 +19,22 @@ export class TaxJarOrderConfirmedAdapter
implements WebhookAdapter<TaxJarOrderConfirmedPayload, TaxJarOrderConfirmedResponse>
{
private logger: Logger;
constructor(private readonly config: TaxJarConfig, private authData: AuthData) {
private readonly config: TaxJarConfig;
private readonly authData: AuthData;
private clientLogger: ClientLogger;
constructor({
config,
authData,
clientLogger,
}: {
config: TaxJarConfig;
clientLogger: ClientLogger;
authData: AuthData;
}) {
this.config = config;
this.authData = authData;
this.clientLogger = clientLogger;
this.logger = createLogger({ name: "TaxJarOrderConfirmedAdapter" });
}
@ -30,14 +46,36 @@ export class TaxJarOrderConfirmedAdapter
this.logger.debug("Calling TaxJar fetchTaxForOrder with transformed payload...");
const client = new TaxJarClient(this.config);
const response = await client.createOrder(target);
this.logger.debug("TaxJar createOrder successfully responded");
const responseTransformer = new TaxJarOrderConfirmedResponseTransformer();
const transformedResponse = responseTransformer.transform(response);
try {
const response = await client.createOrder(target);
this.logger.debug("Transformed TaxJar createOrder response");
this.clientLogger.push({
event: "[OrderConfirmed] createOrder",
status: "success",
payload: {
input: target,
output: response,
},
});
return transformedResponse;
this.logger.debug("TaxJar createOrder successfully responded");
const responseTransformer = new TaxJarOrderConfirmedResponseTransformer();
const transformedResponse = responseTransformer.transform(response);
this.logger.debug("Transformed TaxJar createOrder response");
return transformedResponse;
} catch (error) {
this.clientLogger.push({
event: "[OrderConfirmed] createOrder",
status: "error",
payload: {
input: target,
output: error,
},
});
throw error;
}
}
}

View file

@ -47,7 +47,7 @@ export class TaxJarClient {
}
/**
* In the past, we've had some problems with TaxJar validateAddress. It looks like it works now, but we should keep an eye on it.
* In the past, we've had some problems with TaxJar validateAddress. It looks like only works on production (live token, not sandbox token).
* @see https://github.com/taxjar/taxjar-node/issues/70
*/
async validateAddress({ params }: ValidateAddressArgs) {

View file

@ -2,36 +2,50 @@ import { AuthData } from "@saleor/app-sdk/APL";
import {
OrderCancelledEventSubscriptionFragment,
OrderConfirmedSubscriptionFragment,
TaxBaseFragment,
} from "../../../generated/graphql";
import { Logger, createLogger } from "../../lib/logger";
import { CalculateTaxesPayload } from "../../pages/api/webhooks/checkout-calculate-taxes";
import { ProviderWebhookService } from "../taxes/tax-provider-webhook";
import { TaxJarCalculateTaxesAdapter } from "./calculate-taxes/taxjar-calculate-taxes-adapter";
import { TaxJarOrderConfirmedAdapter } from "./order-confirmed/taxjar-order-confirmed-adapter";
import { TaxJarClient } from "./taxjar-client";
import { TaxJarConfig } from "./taxjar-connection-schema";
import { CalculateTaxesPayload } from "../../pages/api/webhooks/checkout-calculate-taxes";
import { ClientLogger, createClientLogger } from "../logs/client-logger";
export class TaxJarWebhookService implements ProviderWebhookService {
client: TaxJarClient;
private logger: Logger;
private config: TaxJarConfig;
private clientLogger: ClientLogger;
private authData: AuthData;
constructor(
config: TaxJarConfig,
private authData: AuthData,
) {
constructor({
clientLogger,
config,
authData,
}: {
clientLogger: ClientLogger;
config: TaxJarConfig;
authData: AuthData;
}) {
const taxJarClient = new TaxJarClient(config);
this.client = taxJarClient;
this.config = config;
this.authData = authData;
this.clientLogger = clientLogger;
this.logger = createLogger({
name: "TaxJarWebhookService",
});
}
async calculateTaxes(payload: CalculateTaxesPayload) {
const adapter = new TaxJarCalculateTaxesAdapter(this.config, this.authData);
const adapter = new TaxJarCalculateTaxesAdapter({
config: this.config,
authData: this.authData,
clientLogger: this.clientLogger,
});
const response = await adapter.send(payload);
@ -39,7 +53,11 @@ export class TaxJarWebhookService implements ProviderWebhookService {
}
async confirmOrder(order: OrderConfirmedSubscriptionFragment) {
const adapter = new TaxJarOrderConfirmedAdapter(this.config, this.authData);
const adapter = new TaxJarOrderConfirmedAdapter({
config: this.config,
authData: this.authData,
clientLogger: this.clientLogger,
});
const response = await adapter.send({ order });

View file

@ -1,13 +1,14 @@
import { router } from "./trpc-server";
import { providerConnectionsRouter } from "../provider-connections/provider-connections.router";
import { channelsConfigurationRouter } from "../channel-configuration/channel-configuration.router";
import { taxjarConnectionRouter } from "../taxjar/taxjar-connection.router";
import { avataxConnectionRouter } from "../avatax/avatax-connection.router";
import { taxClassesRouter } from "../tax-classes/tax-classes.router";
import { avataxTaxCodesRouter } from "../avatax/tax-code/avatax-tax-codes.router";
import { taxJarTaxCodesRouter } from "../taxjar/tax-code/taxjar-tax-codes.router";
import { taxJarTaxCodeMatchesRouter } from "../taxjar/tax-code/taxjar-tax-code-matches.router";
import { avataxTaxCodeMatchesRouter } from "../avatax/tax-code/avatax-tax-code-matches.router";
import { avataxTaxCodesRouter } from "../avatax/tax-code/avatax-tax-codes.router";
import { channelsConfigurationRouter } from "../channel-configuration/channel-configuration.router";
import { clientLoggerRouter } from "../logs/client-logger.router";
import { providerConnectionsRouter } from "../provider-connections/provider-connections.router";
import { taxClassesRouter } from "../tax-classes/tax-classes.router";
import { taxJarTaxCodeMatchesRouter } from "../taxjar/tax-code/taxjar-tax-code-matches.router";
import { taxJarTaxCodesRouter } from "../taxjar/tax-code/taxjar-tax-codes.router";
import { taxjarConnectionRouter } from "../taxjar/taxjar-connection.router";
import { router } from "./trpc-server";
/*
* // todo: split to namespaces, e.g.:
@ -23,6 +24,7 @@ export const appRouter = router({
taxJarTaxCodes: taxJarTaxCodesRouter,
taxJarMatches: taxJarTaxCodeMatchesRouter,
avataxMatches: avataxTaxCodeMatchesRouter,
clientLogs: clientLoggerRouter,
});
export type AppRouter = typeof appRouter;

View file

@ -1,97 +1,11 @@
import { Breadcrumbs } from "@saleor/apps-ui";
import { useRouter } from "next/router";
type Breadcrumb = {
export type Breadcrumb = {
label: string;
href?: string;
};
const newProviderBreadcrumbs: Breadcrumb[] = [
{
href: "/configuration",
label: "Configuration",
},
{
label: "Providers",
href: "/providers",
},
];
const breadcrumbsForRoute: Record<string, Breadcrumb[]> = {
"/": [],
"/configuration": [
{
href: "/configuration",
label: "Configuration",
},
],
"/providers": [...newProviderBreadcrumbs],
"/providers/taxjar": [
...newProviderBreadcrumbs,
{
label: "TaxJar",
href: "/providers/taxjar",
},
],
"/providers/taxjar/matcher": [
...newProviderBreadcrumbs,
{
label: "TaxJar",
href: "/providers/taxjar",
},
{
label: "Tax code matcher",
href: "/providers/taxjar/matcher",
},
],
"/providers/taxjar/[id]": [
...newProviderBreadcrumbs,
{
label: "Editing TaxJar provider",
href: "/providers/taxjar",
},
],
"/providers/avatax": [
...newProviderBreadcrumbs,
{
label: "AvaTax",
href: "/providers/avatax",
},
],
"/providers/avatax/matcher": [
...newProviderBreadcrumbs,
{
label: "AvaTax",
href: "/providers/avatax",
},
{
label: "Tax code matcher",
href: "/providers/avatax/matcher",
},
],
"/providers/avatax/[id]": [
...newProviderBreadcrumbs,
{
label: "Editing AvaTax provider",
href: "/providers/avatax",
},
],
};
const useBreadcrumbs = () => {
const { pathname } = useRouter();
const breadcrumbs = breadcrumbsForRoute[pathname];
if (pathname !== "/" && pathname !== "_error" && !breadcrumbs) {
throw new Error(`No breadcrumbs for route ${pathname}`);
}
return breadcrumbs;
};
export const AppBreadcrumbs = () => {
const breadcrumbs = useBreadcrumbs();
export const AppBreadcrumbs = ({ breadcrumbs }: { breadcrumbs: Breadcrumb[] }) => {
return (
<Breadcrumbs>
{breadcrumbs.map((breadcrumb) => (

View file

@ -1,11 +1,9 @@
import { Box } from "@saleor/macaw-ui/next";
import React from "react";
import { AppBreadcrumbs } from "./app-breadcrumbs";
export const AppLayout = ({ children }: { children: React.ReactNode }) => {
return (
<Box display={"flex"} flexDirection={"column"} gap={8} paddingY={4}>
<AppBreadcrumbs />
{children}
</Box>
);

View file

@ -0,0 +1,20 @@
import React from "react";
import { AppBreadcrumbs, Breadcrumb } from "./app-breadcrumbs";
import { AppColumns } from "./app-columns";
export const AppPageLayout = ({
top,
children,
breadcrumbs,
}: {
top: React.ReactNode;
children: React.ReactNode;
breadcrumbs: Breadcrumb[];
}) => {
return (
<>
<AppBreadcrumbs breadcrumbs={breadcrumbs} />
<AppColumns top={top}>{children}</AppColumns>
</>
);
};

View file

@ -18,17 +18,21 @@ const Description = ({
description,
...props
}: PropsWithBox<{
title: React.ReactNode;
description: React.ReactNode;
title?: React.ReactNode;
description?: React.ReactNode;
}>) => {
return (
<Box display="flex" flexDirection={"column"} gap={10} __maxWidth={MAX_WIDTH} {...props}>
<Text as="h3" variant="heading">
{title}
</Text>
<Box fontWeight={"bodyMedium"} fontSize={"bodyMedium"}>
{description}
</Box>
{title && (
<Text as="h3" variant="heading">
{title}
</Text>
)}
{description && (
<Box fontWeight={"bodyMedium"} fontSize={"bodyMedium"}>
{description}
</Box>
)}
</Box>
);
};

View file

@ -1,4 +1,4 @@
import { Box, Button } from "@saleor/macaw-ui/next";
import { Box, Button, EditIcon, ViewTableIcon } from "@saleor/macaw-ui/next";
import { useRouter } from "next/router";
import { ProviderConnection } from "../provider-connections/provider-connections";
import { trpcClient } from "../trpc/trpc-client";
@ -9,10 +9,14 @@ export const ProvidersTable = () => {
const router = useRouter();
const { data } = trpcClient.providersConfiguration.getAll.useQuery();
const itemClickHandler = (item: ProviderConnection) => {
const editClickHandler = (item: ProviderConnection) => {
router.push(`/providers/${item.provider}/${item.id}`);
};
const logsClickHandler = (item: ProviderConnection) => {
router.push(`/providers/${item.provider}/${item.id}/logs`);
};
return (
<Table.Container>
<Table.THead color={"textNeutralSubdued"}>
@ -28,9 +32,22 @@ export const ProvidersTable = () => {
<Table.TD>
<ProviderLabel name={item.provider} />
</Table.TD>
<Table.TD onClick={() => itemClickHandler(item)}>
<Box display={"flex"} justifyContent={"flex-end"}>
<Button data-testid="provider-edit-button" variant="tertiary">
<Table.TD>
<Box display={"flex"} justifyContent={"flex-end"} gap={2}>
<Button
onClick={() => logsClickHandler(item)}
icon={<ViewTableIcon />}
variant="tertiary"
data-testid="provider-logs-button"
>
Logs
</Button>
<Button
onClick={() => editClickHandler(item)}
icon={<EditIcon />}
data-testid="provider-edit-button"
variant="secondary"
>
Edit
</Button>
</Box>

View file

@ -1,4 +1,27 @@
import { Box, BoxProps } from "@saleor/macaw-ui/next";
import { Box, BoxProps, Skeleton } from "@saleor/macaw-ui/next";
const TableSkeletonRow = () => {
return (
<Box display="grid" gridTemplateColumns={3} gap={8}>
<Skeleton __width={"100%"} height={6} />
<Skeleton __width={"100%"} height={6} />
<Skeleton __width={"100%"} height={6} />
</Box>
);
};
const TableSkeleton = () => {
return (
<Box display="grid" gap={4} marginTop={4}>
<Skeleton __width={"100%"} height={6} />
<Box display="grid" gap={4}>
<TableSkeletonRow />
<TableSkeletonRow />
<TableSkeletonRow />
</Box>
</Box>
);
};
export const Table = {
Container: (props: BoxProps) => (
@ -11,4 +34,5 @@ export const Table = {
),
TBody: (props: BoxProps) => <Box {...props} as="tbody" />,
TD: (props: BoxProps) => <Box fontSize="bodyMedium" paddingTop={2} {...props} as="td" />,
Skeleton: () => <TableSkeleton />,
};

View file

@ -20,7 +20,7 @@ export type CalculateTaxesPayload = Extract<
>;
function verifyCalculateTaxesPayload(payload: CalculateTaxesPayload) {
if (!payload.taxBase.lines) {
if (!payload.taxBase.lines.length) {
throw new Error("No lines found in taxBase");
}
@ -48,7 +48,7 @@ export default checkoutCalculateTaxesSyncWebhook.createHandler(async (req, res,
try {
verifyCalculateTaxesPayload(payload);
logger.debug("Payload validated succesfully");
logger.debug("Payload validated Successfully");
const appMetadata = payload.recipient?.privateMetadata ?? [];
const channelSlug = payload.taxBase.channel.slug;

View file

@ -17,7 +17,7 @@ export const config = {
type CalculateTaxesPayload = Extract<CalculateTaxesEventFragment, { __typename: "CalculateTaxes" }>;
function verifyCalculateTaxesPayload(payload: CalculateTaxesPayload) {
if (!payload.taxBase.lines) {
if (!payload.taxBase.lines.length) {
throw new Error("No lines found in taxBase");
}

View file

@ -1,6 +1,7 @@
import { ChannelSection } from "../modules/channel-configuration/ui/channel-section";
import { ProvidersSection } from "../modules/provider-connections/ui/providers-section";
import { AppColumns } from "../modules/ui/app-columns";
import { AppPageLayout } from "../modules/ui/app-page-layout";
import { Section } from "../modules/ui/app-section";
import { MatcherSection } from "../modules/ui/matcher-section";
@ -14,11 +15,19 @@ const Header = () => {
const ConfigurationPage = () => {
return (
<AppColumns top={<Header />}>
<AppPageLayout
top={<Header />}
breadcrumbs={[
{
href: "/configuration",
label: "Configuration",
},
]}
>
<ProvidersSection />
<ChannelSection />
<MatcherSection />
</AppColumns>
</AppPageLayout>
);
};

View file

@ -1,24 +0,0 @@
import { Provider } from "jotai";
import { AvataxInstructions } from "../../../modules/avatax/ui/avatax-instructions";
import { EditAvataxConfiguration } from "../../../modules/avatax/ui/edit-avatax-configuration";
import { AppColumns } from "../../../modules/ui/app-columns";
import { Section } from "../../../modules/ui/app-section";
const Header = () => {
return <Section.Header>Edit your existing AvaTax configuration</Section.Header>;
};
const EditAvataxPage = () => {
return (
<main>
<AppColumns top={<Header />}>
<AvataxInstructions />
<Provider>
<EditAvataxConfiguration />
</Provider>
</AppColumns>
</main>
);
};
export default EditAvataxPage;

View file

@ -0,0 +1,46 @@
import { Provider } from "jotai";
import { useRouter } from "next/router";
import { AvataxInstructions } from "../../../../modules/avatax/ui/avatax-instructions";
import { EditAvataxConfiguration } from "../../../../modules/avatax/ui/edit-avatax-configuration";
import { AppPageLayout } from "../../../../modules/ui/app-page-layout";
import { Section } from "../../../../modules/ui/app-section";
const Header = () => {
return <Section.Header>Edit your existing AvaTax configuration</Section.Header>;
};
const EditAvataxPage = () => {
const router = useRouter();
const { id } = router.query;
return (
<AppPageLayout
breadcrumbs={[
{
href: "/configuration",
label: "Configuration",
},
{
href: "/providers",
label: "Providers",
},
{
href: "/providers/avatax",
label: "AvaTax",
},
{
href: `/providers/avatax/${id}`,
label: String(id),
},
]}
top={<Header />}
>
<AvataxInstructions />
<Provider>
<EditAvataxConfiguration />
</Provider>
</AppPageLayout>
);
};
export default EditAvataxPage;

View file

@ -0,0 +1,45 @@
import { useRouter } from "next/router";
import { ClientLogsPage } from "../../../../modules/logs/ui/client-logs-page";
import { AppPageLayout } from "../../../../modules/ui/app-page-layout";
import { Section } from "../../../../modules/ui/app-section";
const Header = () => {
return <Section.Header>Display logs for your configuration</Section.Header>;
};
const LogsAvataxPage = () => {
const router = useRouter();
const { id } = router.query;
return (
<AppPageLayout
breadcrumbs={[
{
href: "/configuration",
label: "Configuration",
},
{
href: "/providers",
label: "Providers",
},
{
href: "/providers/avatax",
label: "AvaTax",
},
{
href: `/providers/avatax/${id}`,
label: String(id),
},
{
href: `/providers/avatax/${id}/logs`,
label: "Logs",
},
]}
top={<Header />}
>
<ClientLogsPage />
</AppPageLayout>
);
};
export default LogsAvataxPage;

View file

@ -2,7 +2,7 @@ import { Box, Text } from "@saleor/macaw-ui/next";
import { Provider } from "jotai";
import { AvataxInstructions } from "../../../modules/avatax/ui/avatax-instructions";
import { CreateAvataxConfiguration } from "../../../modules/avatax/ui/create-avatax-configuration";
import { AppColumns } from "../../../modules/ui/app-columns";
import { AppPageLayout } from "../../../modules/ui/app-page-layout";
const Header = () => {
return (
@ -16,14 +16,28 @@ const Header = () => {
const NewAvataxPage = () => {
return (
<main>
<AppColumns top={<Header />}>
<AvataxInstructions />
<Provider>
<CreateAvataxConfiguration />
</Provider>
</AppColumns>
</main>
<AppPageLayout
breadcrumbs={[
{
href: "/configuration",
label: "Configuration",
},
{
href: "/providers",
label: "Providers",
},
{
href: "/providers/avatax",
label: "AvaTax",
},
]}
top={<Header />}
>
<AvataxInstructions />
<Provider>
<CreateAvataxConfiguration />
</Provider>
</AppPageLayout>
);
};

View file

@ -5,6 +5,7 @@ import { AppCard } from "../../modules/ui/app-card";
import { AppColumns } from "../../modules/ui/app-columns";
import { ProviderLabel } from "../../modules/ui/provider-label";
import { Section } from "../../modules/ui/app-section";
import { AppPageLayout } from "../../modules/ui/app-page-layout";
const Header = () => {
return (
@ -107,12 +108,22 @@ const ChooseProvider = () => {
const NewProviderPage = () => {
return (
<main>
<AppColumns top={<Header />}>
<Intro />
<ChooseProvider />
</AppColumns>
</main>
<AppPageLayout
breadcrumbs={[
{
href: "/configuration",
label: "Configuration",
},
{
href: "/providers",
label: "Providers",
},
]}
top={<Header />}
>
<Intro />
<ChooseProvider />
</AppPageLayout>
);
};

View file

@ -1,27 +0,0 @@
import { Box, Text } from "@saleor/macaw-ui/next";
import { EditTaxJarConfiguration } from "../../../modules/taxjar/ui/edit-taxjar-configuration";
import { AppColumns } from "../../../modules/ui/app-columns";
import { TaxJarInstructions } from "../../../modules/taxjar/ui/taxjar-instructions";
const Header = () => {
return (
<Box>
<Text as="p" variant="body">
Edit your existing TaxJar configuration
</Text>
</Box>
);
};
const EditTaxJarPage = () => {
return (
<main>
<AppColumns top={<Header />}>
<TaxJarInstructions />
<EditTaxJarConfiguration />
</AppColumns>
</main>
);
};
export default EditTaxJarPage;

View file

@ -0,0 +1,49 @@
import { Box, Text } from "@saleor/macaw-ui/next";
import { useRouter } from "next/router";
import { EditTaxJarConfiguration } from "../../../../modules/taxjar/ui/edit-taxjar-configuration";
import { TaxJarInstructions } from "../../../../modules/taxjar/ui/taxjar-instructions";
import { AppPageLayout } from "../../../../modules/ui/app-page-layout";
const Header = () => {
return (
<Box>
<Text as="p" variant="body">
Edit your existing TaxJar configuration
</Text>
</Box>
);
};
const EditTaxJarPage = () => {
const router = useRouter();
const { id } = router.query;
return (
<AppPageLayout
breadcrumbs={[
{
href: "/configuration",
label: "Configuration",
},
{
href: "/providers",
label: "Providers",
},
{
href: "/providers/taxjar",
label: "TaxJar",
},
{
href: `/providers/taxjar/${id}`,
label: String(id),
},
]}
top={<Header />}
>
<TaxJarInstructions />
<EditTaxJarConfiguration />
</AppPageLayout>
);
};
export default EditTaxJarPage;

View file

@ -0,0 +1,45 @@
import { useRouter } from "next/router";
import { ClientLogsPage } from "../../../../modules/logs/ui/client-logs-page";
import { AppPageLayout } from "../../../../modules/ui/app-page-layout";
import { Section } from "../../../../modules/ui/app-section";
const Header = () => {
return <Section.Header>Display logs for your configuration</Section.Header>;
};
const LogsTaxjarPage = () => {
const router = useRouter();
const { id } = router.query;
return (
<AppPageLayout
breadcrumbs={[
{
href: "/configuration",
label: "Configuration",
},
{
href: "/providers",
label: "Providers",
},
{
href: "/providers/taxjar",
label: "TaxJar",
},
{
href: `/providers/taxjar/${id}`,
label: String(id),
},
{
href: `/providers/taxjar/${id}/logs`,
label: "Logs",
},
]}
top={<Header />}
>
<ClientLogsPage />
</AppPageLayout>
);
};
export default LogsTaxjarPage;

View file

@ -1,7 +1,7 @@
import { Box, Text } from "@saleor/macaw-ui/next";
import { AppColumns } from "../../../modules/ui/app-columns";
import { CreateTaxJarConfiguration } from "../../../modules/taxjar/ui/create-taxjar-configuration";
import { TaxJarInstructions } from "../../../modules/taxjar/ui/taxjar-instructions";
import { AppPageLayout } from "../../../modules/ui/app-page-layout";
const Header = () => {
return (
@ -15,12 +15,26 @@ const Header = () => {
const NewTaxJarPage = () => {
return (
<main>
<AppColumns top={<Header />}>
<TaxJarInstructions />
<CreateTaxJarConfiguration />
</AppColumns>
</main>
<AppPageLayout
top={<Header />}
breadcrumbs={[
{
href: "/configuration",
label: "Configuration",
},
{
href: "/providers",
label: "Providers",
},
{
href: "/providers/taxjar",
label: "TaxJar",
},
]}
>
<TaxJarInstructions />
<CreateTaxJarConfiguration />
</AppPageLayout>
);
};