diff --git a/.changeset/honest-geese-crash.md b/.changeset/honest-geese-crash.md new file mode 100644 index 0000000..a4e3680 --- /dev/null +++ b/.changeset/honest-geese-crash.md @@ -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. diff --git a/apps/taxes/src/modules/avatax/avatax-webhook.service.ts b/apps/taxes/src/modules/avatax/avatax-webhook.service.ts index 95a46aa..b5a41ab 100644 --- a/apps/taxes/src/modules/avatax/avatax-webhook.service.ts +++ b/apps/taxes/src/modules/avatax/avatax-webhook.service.ts @@ -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); } diff --git a/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-adapter.ts b/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-adapter.ts index 5435a31..9b98c47 100644 --- a/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-adapter.ts +++ b/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-adapter.ts @@ -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 { 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 { - 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; + } } } diff --git a/apps/taxes/src/modules/avatax/order-cancelled/avatax-order-cancelled-adapter.ts b/apps/taxes/src/modules/avatax/order-cancelled/avatax-order-cancelled-adapter.ts index 6895365..47eca4b 100644 --- a/apps/taxes/src/modules/avatax/order-cancelled/avatax-order-cancelled-adapter.ts +++ b/apps/taxes/src/modules/avatax/order-cancelled/avatax-order-cancelled-adapter.ts @@ -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 { 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 { 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 { @@ -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; + } } } diff --git a/apps/taxes/src/modules/logs/client-logger.router.ts b/apps/taxes/src/modules/logs/client-logger.router.ts new file mode 100644 index 0000000..82e41df --- /dev/null +++ b/apps/taxes/src/modules/logs/client-logger.router.ts @@ -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(); + }), +}); diff --git a/apps/taxes/src/modules/logs/client-logger.ts b/apps/taxes/src/modules/logs/client-logger.ts new file mode 100644 index 0000000..29dd36a --- /dev/null +++ b/apps/taxes/src/modules/logs/client-logger.ts @@ -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; + +export type ClientLog = z.infer; + +export const LOG_LIMIT = 100; + +export class ClientLogger implements MetadataLogs { + private logRepository: ClientLogsMetadataRepository; + + constructor({ + settingsManager, + configurationId, + }: Pick, "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, + }); +} diff --git a/apps/taxes/src/modules/logs/client-logs-metadata-repository.test.ts b/apps/taxes/src/modules/logs/client-logs-metadata-repository.test.ts new file mode 100644 index 0000000..6c7a324 --- /dev/null +++ b/apps/taxes/src/modules/logs/client-logs-metadata-repository.test.ts @@ -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" }]); + }); +}); diff --git a/apps/taxes/src/modules/logs/client-logs-metadata-repository.ts b/apps/taxes/src/modules/logs/client-logs-metadata-repository.ts new file mode 100644 index 0000000..99805dc --- /dev/null +++ b/apps/taxes/src/modules/logs/client-logs-metadata-repository.ts @@ -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 { + getAll(): Promise; + push(payload: unknown): Promise; +} + +type ClientLogsMetadataRepositoryOptions = { + limit: number; +}; + +export type ClientLogsMetadataRepositoryParams = { + settingsManager: EncryptedMetadataManager; + schema: ZodSchema; + 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 implements MetadataLogs { + private readonly settingsManager: EncryptedMetadataManager; + private readonly schema: ZodSchema; + private readonly metadataKey: string; + private readonly options: ClientLogsMetadataRepositoryOptions; + private readonly logger: Logger; + private logs: TLog[] = []; + + constructor({ + settingsManager, + schema, + metadataKey, + options, + }: ClientLogsMetadataRepositoryParams) { + 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), + }); + } +} diff --git a/apps/taxes/src/modules/logs/log-utils.test.ts b/apps/taxes/src/modules/logs/log-utils.test.ts new file mode 100644 index 0000000..d2febc3 --- /dev/null +++ b/apps/taxes/src/modules/logs/log-utils.test.ts @@ -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(); + }); + }); +}); diff --git a/apps/taxes/src/modules/logs/log-utils.ts b/apps/taxes/src/modules/logs/log-utils.ts new file mode 100644 index 0000000..19378ca --- /dev/null +++ b/apps/taxes/src/modules/logs/log-utils.ts @@ -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(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, +}; diff --git a/apps/taxes/src/modules/logs/ui/client-logs-page.tsx b/apps/taxes/src/modules/logs/ui/client-logs-page.tsx new file mode 100644 index 0000000..528143f --- /dev/null +++ b/apps/taxes/src/modules/logs/ui/client-logs-page.tsx @@ -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 ( + + + Taxes App records all API calls and responses. You can use this information to debug + issues with your provider. + + + Only the last {LOG_LIMIT} logs are stored. + + + The naming convention for each event is: [WebhookName] API method name. + + + } + /> + ); +}; + +export const ClientLogsPage = () => { + return ( + <> + + + + + + ); +}; diff --git a/apps/taxes/src/modules/logs/ui/client-logs-table.tsx b/apps/taxes/src/modules/logs/ui/client-logs-table.tsx new file mode 100644 index 0000000..2e593e8 --- /dev/null +++ b/apps/taxes/src/modules/logs/ui/client-logs-table.tsx @@ -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[0]; + +const chipVariantMap: Record = { + success: "success", + error: "error", +}; + +const StatusCell = ({ status }: { status: ClientLog["status"] }) => { + return {status}; +}; + +const LogRow = ({ log }: { log: ClientLog }) => { + return ( + <> + {log.event} + + + + {log.date} + + ); +}; + +const LogAccordion = ({ log }: { log: ClientLog }) => { + if (log.payload) { + const prettyPayload = JSON.stringify(JSON.parse(log.payload), null, 2); + + return ( + + + + + + + + + + + + + + {prettyPayload} + + + + + + ); + } + + return ( + + + + ); +}; + +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 ( + + {logs.map((log, index, array) => { + const isLast = index === array.length - 1; + + return ( + <> + + {!isLast && } + + ); + })} + {isEmpty && ( + + + No logs found for this configuration + + + )} + {isLoading && } + + ); +}; diff --git a/apps/taxes/src/modules/logs/ui/client-logs.tsx b/apps/taxes/src/modules/logs/ui/client-logs.tsx new file mode 100644 index 0000000..58e877f --- /dev/null +++ b/apps/taxes/src/modules/logs/ui/client-logs.tsx @@ -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 ( + + ); +}; + +export const ClientLogs = () => { + return ( +
+ + + Logs + + + + +
+ ); +}; diff --git a/apps/taxes/src/modules/taxes/get-active-connection-service.ts b/apps/taxes/src/modules/taxes/get-active-connection-service.ts index 35fb7e5..3dca1ad 100644 --- a/apps/taxes/src/modules/taxes/get-active-connection-service.ts +++ b/apps/taxes/src/modules/taxes/get-active-connection-service.ts @@ -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; } diff --git a/apps/taxes/src/modules/taxjar/calculate-taxes/taxjar-calculate-taxes-adapter.ts b/apps/taxes/src/modules/taxjar/calculate-taxes/taxjar-calculate-taxes-adapter.ts index 308a38a..ecd2651 100644 --- a/apps/taxes/src/modules/taxjar/calculate-taxes/taxjar-calculate-taxes-adapter.ts +++ b/apps/taxes/src/modules/taxjar/calculate-taxes/taxjar-calculate-taxes-adapter.ts @@ -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 { 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; + } } } diff --git a/apps/taxes/src/modules/taxjar/configuration/taxjar-validation.service.ts b/apps/taxes/src/modules/taxjar/configuration/taxjar-validation.service.ts index 77fbf09..7719e05 100644 --- a/apps/taxes/src/modules/taxjar/configuration/taxjar-validation.service.ts +++ b/apps/taxes/src/modules/taxjar/configuration/taxjar-validation.service.ts @@ -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 { 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); diff --git a/apps/taxes/src/modules/taxjar/order-confirmed/taxjar-order-confirmed-adapter.ts b/apps/taxes/src/modules/taxjar/order-confirmed/taxjar-order-confirmed-adapter.ts index f5aa737..08d047f 100644 --- a/apps/taxes/src/modules/taxjar/order-confirmed/taxjar-order-confirmed-adapter.ts +++ b/apps/taxes/src/modules/taxjar/order-confirmed/taxjar-order-confirmed-adapter.ts @@ -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 { 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; + } } } diff --git a/apps/taxes/src/modules/taxjar/taxjar-client.ts b/apps/taxes/src/modules/taxjar/taxjar-client.ts index 90349ff..9c7920a 100644 --- a/apps/taxes/src/modules/taxjar/taxjar-client.ts +++ b/apps/taxes/src/modules/taxjar/taxjar-client.ts @@ -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) { diff --git a/apps/taxes/src/modules/taxjar/taxjar-webhook.service.ts b/apps/taxes/src/modules/taxjar/taxjar-webhook.service.ts index 3a742bb..12e3d57 100644 --- a/apps/taxes/src/modules/taxjar/taxjar-webhook.service.ts +++ b/apps/taxes/src/modules/taxjar/taxjar-webhook.service.ts @@ -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 }); diff --git a/apps/taxes/src/modules/trpc/trpc-app-router.ts b/apps/taxes/src/modules/trpc/trpc-app-router.ts index 3d01ca9..8fe4411 100644 --- a/apps/taxes/src/modules/trpc/trpc-app-router.ts +++ b/apps/taxes/src/modules/trpc/trpc-app-router.ts @@ -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; diff --git a/apps/taxes/src/modules/ui/app-breadcrumbs.tsx b/apps/taxes/src/modules/ui/app-breadcrumbs.tsx index f94e328..604dc0f 100644 --- a/apps/taxes/src/modules/ui/app-breadcrumbs.tsx +++ b/apps/taxes/src/modules/ui/app-breadcrumbs.tsx @@ -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 = { - "/": [], - "/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.map((breadcrumb) => ( diff --git a/apps/taxes/src/modules/ui/app-layout.tsx b/apps/taxes/src/modules/ui/app-layout.tsx index 7e83cc6..eaaba14 100644 --- a/apps/taxes/src/modules/ui/app-layout.tsx +++ b/apps/taxes/src/modules/ui/app-layout.tsx @@ -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 ( - {children} ); diff --git a/apps/taxes/src/modules/ui/app-page-layout.tsx b/apps/taxes/src/modules/ui/app-page-layout.tsx new file mode 100644 index 0000000..cd9d4ee --- /dev/null +++ b/apps/taxes/src/modules/ui/app-page-layout.tsx @@ -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 ( + <> + + {children} + + ); +}; diff --git a/apps/taxes/src/modules/ui/app-section.tsx b/apps/taxes/src/modules/ui/app-section.tsx index f2bf096..d09f8aa 100644 --- a/apps/taxes/src/modules/ui/app-section.tsx +++ b/apps/taxes/src/modules/ui/app-section.tsx @@ -18,17 +18,21 @@ const Description = ({ description, ...props }: PropsWithBox<{ - title: React.ReactNode; - description: React.ReactNode; + title?: React.ReactNode; + description?: React.ReactNode; }>) => { return ( - - {title} - - - {description} - + {title && ( + + {title} + + )} + {description && ( + + {description} + + )} ); }; diff --git a/apps/taxes/src/modules/ui/providers-table.tsx b/apps/taxes/src/modules/ui/providers-table.tsx index 127a0d1..3d80ac0 100644 --- a/apps/taxes/src/modules/ui/providers-table.tsx +++ b/apps/taxes/src/modules/ui/providers-table.tsx @@ -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 ( @@ -28,9 +32,22 @@ export const ProvidersTable = () => { - itemClickHandler(item)}> - - + diff --git a/apps/taxes/src/modules/ui/table.tsx b/apps/taxes/src/modules/ui/table.tsx index 80e35ff..bba0ef4 100644 --- a/apps/taxes/src/modules/ui/table.tsx +++ b/apps/taxes/src/modules/ui/table.tsx @@ -1,4 +1,27 @@ -import { Box, BoxProps } from "@saleor/macaw-ui/next"; +import { Box, BoxProps, Skeleton } from "@saleor/macaw-ui/next"; + +const TableSkeletonRow = () => { + return ( + + + + + + ); +}; + +const TableSkeleton = () => { + return ( + + + + + + + + + ); +}; export const Table = { Container: (props: BoxProps) => ( @@ -11,4 +34,5 @@ export const Table = { ), TBody: (props: BoxProps) => , TD: (props: BoxProps) => , + Skeleton: () => , }; diff --git a/apps/taxes/src/pages/api/webhooks/checkout-calculate-taxes.ts b/apps/taxes/src/pages/api/webhooks/checkout-calculate-taxes.ts index 092b244..3cd1956 100644 --- a/apps/taxes/src/pages/api/webhooks/checkout-calculate-taxes.ts +++ b/apps/taxes/src/pages/api/webhooks/checkout-calculate-taxes.ts @@ -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; diff --git a/apps/taxes/src/pages/api/webhooks/order-calculate-taxes.ts b/apps/taxes/src/pages/api/webhooks/order-calculate-taxes.ts index 5501495..4090a3c 100644 --- a/apps/taxes/src/pages/api/webhooks/order-calculate-taxes.ts +++ b/apps/taxes/src/pages/api/webhooks/order-calculate-taxes.ts @@ -17,7 +17,7 @@ export const config = { type CalculateTaxesPayload = Extract; function verifyCalculateTaxesPayload(payload: CalculateTaxesPayload) { - if (!payload.taxBase.lines) { + if (!payload.taxBase.lines.length) { throw new Error("No lines found in taxBase"); } diff --git a/apps/taxes/src/pages/configuration.tsx b/apps/taxes/src/pages/configuration.tsx index 2f8026f..ed57950 100644 --- a/apps/taxes/src/pages/configuration.tsx +++ b/apps/taxes/src/pages/configuration.tsx @@ -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 ( - }> + } + breadcrumbs={[ + { + href: "/configuration", + label: "Configuration", + }, + ]} + > - + ); }; diff --git a/apps/taxes/src/pages/providers/avatax/[id].tsx b/apps/taxes/src/pages/providers/avatax/[id].tsx deleted file mode 100644 index 208ad33..0000000 --- a/apps/taxes/src/pages/providers/avatax/[id].tsx +++ /dev/null @@ -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 Edit your existing AvaTax configuration; -}; - -const EditAvataxPage = () => { - return ( -
- }> - - - - - -
- ); -}; - -export default EditAvataxPage; diff --git a/apps/taxes/src/pages/providers/avatax/[id]/index.tsx b/apps/taxes/src/pages/providers/avatax/[id]/index.tsx new file mode 100644 index 0000000..30334c6 --- /dev/null +++ b/apps/taxes/src/pages/providers/avatax/[id]/index.tsx @@ -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 Edit your existing AvaTax configuration; +}; + +const EditAvataxPage = () => { + const router = useRouter(); + const { id } = router.query; + + return ( + } + > + + + + + + ); +}; + +export default EditAvataxPage; diff --git a/apps/taxes/src/pages/providers/avatax/[id]/logs.tsx b/apps/taxes/src/pages/providers/avatax/[id]/logs.tsx new file mode 100644 index 0000000..cadd0bc --- /dev/null +++ b/apps/taxes/src/pages/providers/avatax/[id]/logs.tsx @@ -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 Display logs for your configuration; +}; + +const LogsAvataxPage = () => { + const router = useRouter(); + const { id } = router.query; + + return ( + } + > + + + ); +}; + +export default LogsAvataxPage; diff --git a/apps/taxes/src/pages/providers/avatax/index.tsx b/apps/taxes/src/pages/providers/avatax/index.tsx index 994a333..1287d47 100644 --- a/apps/taxes/src/pages/providers/avatax/index.tsx +++ b/apps/taxes/src/pages/providers/avatax/index.tsx @@ -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 ( -
- }> - - - - - -
+ } + > + + + + + ); }; diff --git a/apps/taxes/src/pages/providers/index.tsx b/apps/taxes/src/pages/providers/index.tsx index 2b92065..eac6f42 100644 --- a/apps/taxes/src/pages/providers/index.tsx +++ b/apps/taxes/src/pages/providers/index.tsx @@ -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 ( -
- }> - - - -
+ } + > + + + ); }; diff --git a/apps/taxes/src/pages/providers/taxjar/[id].tsx b/apps/taxes/src/pages/providers/taxjar/[id].tsx deleted file mode 100644 index 1ab8c85..0000000 --- a/apps/taxes/src/pages/providers/taxjar/[id].tsx +++ /dev/null @@ -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 ( - - - Edit your existing TaxJar configuration - - - ); -}; - -const EditTaxJarPage = () => { - return ( -
- }> - - - -
- ); -}; - -export default EditTaxJarPage; diff --git a/apps/taxes/src/pages/providers/taxjar/[id]/index.tsx b/apps/taxes/src/pages/providers/taxjar/[id]/index.tsx new file mode 100644 index 0000000..f578883 --- /dev/null +++ b/apps/taxes/src/pages/providers/taxjar/[id]/index.tsx @@ -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 ( + + + Edit your existing TaxJar configuration + + + ); +}; + +const EditTaxJarPage = () => { + const router = useRouter(); + const { id } = router.query; + + return ( + } + > + + + + ); +}; + +export default EditTaxJarPage; diff --git a/apps/taxes/src/pages/providers/taxjar/[id]/logs.tsx b/apps/taxes/src/pages/providers/taxjar/[id]/logs.tsx new file mode 100644 index 0000000..d210343 --- /dev/null +++ b/apps/taxes/src/pages/providers/taxjar/[id]/logs.tsx @@ -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 Display logs for your configuration; +}; + +const LogsTaxjarPage = () => { + const router = useRouter(); + const { id } = router.query; + + return ( + } + > + + + ); +}; + +export default LogsTaxjarPage; diff --git a/apps/taxes/src/pages/providers/taxjar/index.tsx b/apps/taxes/src/pages/providers/taxjar/index.tsx index 8ff1ba4..988c7eb 100644 --- a/apps/taxes/src/pages/providers/taxjar/index.tsx +++ b/apps/taxes/src/pages/providers/taxjar/index.tsx @@ -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 ( -
- }> - - - -
+ } + breadcrumbs={[ + { + href: "/configuration", + label: "Configuration", + }, + { + href: "/providers", + label: "Providers", + }, + { + href: "/providers/taxjar", + label: "TaxJar", + }, + ]} + > + + + ); };