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:
parent
4a2275e999
commit
a32fe7caf4
39 changed files with 1264 additions and 261 deletions
5
.changeset/honest-geese-crash.md
Normal file
5
.changeset/honest-geese-crash.md
Normal 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.
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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,26 +18,46 @@ 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);
|
||||
|
||||
try {
|
||||
const response = await client.createTransaction(target);
|
||||
|
||||
this.clientLogger.push({
|
||||
event: "[CalculateTaxes] createTransaction",
|
||||
status: "success",
|
||||
payload: {
|
||||
input: target,
|
||||
output: response,
|
||||
},
|
||||
});
|
||||
|
||||
this.logger.debug("AvaTax createTransaction successfully responded");
|
||||
|
||||
const responseTransformer = new AvataxCalculateTaxesResponseTransformer();
|
||||
|
@ -45,5 +66,16 @@ export class AvataxCalculateTaxesAdapter
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,8 +46,19 @@ export class AvataxOrderConfirmedAdapter
|
|||
this.logger.debug("Calling AvaTax createTransaction with transformed payload...");
|
||||
|
||||
const client = new AvataxClient(this.config);
|
||||
|
||||
try {
|
||||
const response = await client.createTransaction(target);
|
||||
|
||||
this.clientLogger.push({
|
||||
event: "[OrderConfirmed] createTransaction",
|
||||
status: "success",
|
||||
payload: {
|
||||
input: target,
|
||||
output: response,
|
||||
},
|
||||
});
|
||||
|
||||
this.logger.debug("AvaTax createTransaction successfully responded");
|
||||
|
||||
const responseTransformer = new AvataxOrderConfirmedResponseTransformer();
|
||||
|
@ -41,5 +67,16 @@ export class AvataxOrderConfirmedAdapter
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
57
apps/taxes/src/modules/logs/client-logger.router.ts
Normal file
57
apps/taxes/src/modules/logs/client-logger.router.ts
Normal 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();
|
||||
}),
|
||||
});
|
88
apps/taxes/src/modules/logs/client-logger.ts
Normal file
88
apps/taxes/src/modules/logs/client-logger.ts
Normal 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,
|
||||
});
|
||||
}
|
|
@ -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" }]);
|
||||
});
|
||||
});
|
|
@ -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),
|
||||
});
|
||||
}
|
||||
}
|
38
apps/taxes/src/modules/logs/log-utils.test.ts
Normal file
38
apps/taxes/src/modules/logs/log-utils.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
24
apps/taxes/src/modules/logs/log-utils.ts
Normal file
24
apps/taxes/src/modules/logs/log-utils.ts
Normal 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,
|
||||
};
|
37
apps/taxes/src/modules/logs/ui/client-logs-page.tsx
Normal file
37
apps/taxes/src/modules/logs/ui/client-logs-page.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
122
apps/taxes/src/modules/logs/ui/client-logs-table.tsx
Normal file
122
apps/taxes/src/modules/logs/ui/client-logs-table.tsx
Normal 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>
|
||||
);
|
||||
};
|
35
apps/taxes/src/modules/logs/ui/client-logs.tsx
Normal file
35
apps/taxes/src/modules/logs/ui/client-logs.tsx
Normal 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>
|
||||
);
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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,8 +48,19 @@ export class TaxJarCalculateTaxesAdapter
|
|||
this.logger.debug("Calling TaxJar fetchTaxForOrder with transformed payload...");
|
||||
|
||||
const client = new TaxJarClient(this.config);
|
||||
|
||||
try {
|
||||
const response = await client.fetchTaxForOrder(target);
|
||||
|
||||
this.clientLogger.push({
|
||||
event: "[CalculateTaxes] fetchTaxForOrder",
|
||||
status: "success",
|
||||
payload: {
|
||||
input: target,
|
||||
output: response,
|
||||
},
|
||||
});
|
||||
|
||||
this.logger.debug("TaxJar fetchTaxForOrder responded with:");
|
||||
|
||||
const responseTransformer = new TaxJarCalculateTaxesResponseTransformer();
|
||||
|
@ -42,5 +69,16 @@ export class TaxJarCalculateTaxesAdapter
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,8 +46,19 @@ export class TaxJarOrderConfirmedAdapter
|
|||
this.logger.debug("Calling TaxJar fetchTaxForOrder with transformed payload...");
|
||||
|
||||
const client = new TaxJarClient(this.config);
|
||||
|
||||
try {
|
||||
const response = await client.createOrder(target);
|
||||
|
||||
this.clientLogger.push({
|
||||
event: "[OrderConfirmed] createOrder",
|
||||
status: "success",
|
||||
payload: {
|
||||
input: target,
|
||||
output: response,
|
||||
},
|
||||
});
|
||||
|
||||
this.logger.debug("TaxJar createOrder successfully responded");
|
||||
const responseTransformer = new TaxJarOrderConfirmedResponseTransformer();
|
||||
const transformedResponse = responseTransformer.transform(response);
|
||||
|
@ -39,5 +66,16 @@ export class TaxJarOrderConfirmedAdapter
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 });
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) => (
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
20
apps/taxes/src/modules/ui/app-page-layout.tsx
Normal file
20
apps/taxes/src/modules/ui/app-page-layout.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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}>
|
||||
{title && (
|
||||
<Text as="h3" variant="heading">
|
||||
{title}
|
||||
</Text>
|
||||
)}
|
||||
{description && (
|
||||
<Box fontWeight={"bodyMedium"} fontSize={"bodyMedium"}>
|
||||
{description}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 />,
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
46
apps/taxes/src/pages/providers/avatax/[id]/index.tsx
Normal file
46
apps/taxes/src/pages/providers/avatax/[id]/index.tsx
Normal 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;
|
45
apps/taxes/src/pages/providers/avatax/[id]/logs.tsx
Normal file
45
apps/taxes/src/pages/providers/avatax/[id]/logs.tsx
Normal 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;
|
|
@ -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 />}>
|
||||
<AppPageLayout
|
||||
breadcrumbs={[
|
||||
{
|
||||
href: "/configuration",
|
||||
label: "Configuration",
|
||||
},
|
||||
{
|
||||
href: "/providers",
|
||||
label: "Providers",
|
||||
},
|
||||
{
|
||||
href: "/providers/avatax",
|
||||
label: "AvaTax",
|
||||
},
|
||||
]}
|
||||
top={<Header />}
|
||||
>
|
||||
<AvataxInstructions />
|
||||
<Provider>
|
||||
<CreateAvataxConfiguration />
|
||||
</Provider>
|
||||
</AppColumns>
|
||||
</main>
|
||||
</AppPageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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 />}>
|
||||
<AppPageLayout
|
||||
breadcrumbs={[
|
||||
{
|
||||
href: "/configuration",
|
||||
label: "Configuration",
|
||||
},
|
||||
{
|
||||
href: "/providers",
|
||||
label: "Providers",
|
||||
},
|
||||
]}
|
||||
top={<Header />}
|
||||
>
|
||||
<Intro />
|
||||
<ChooseProvider />
|
||||
</AppColumns>
|
||||
</main>
|
||||
</AppPageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
49
apps/taxes/src/pages/providers/taxjar/[id]/index.tsx
Normal file
49
apps/taxes/src/pages/providers/taxjar/[id]/index.tsx
Normal 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;
|
45
apps/taxes/src/pages/providers/taxjar/[id]/logs.tsx
Normal file
45
apps/taxes/src/pages/providers/taxjar/[id]/logs.tsx
Normal 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;
|
|
@ -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 />}>
|
||||
<AppPageLayout
|
||||
top={<Header />}
|
||||
breadcrumbs={[
|
||||
{
|
||||
href: "/configuration",
|
||||
label: "Configuration",
|
||||
},
|
||||
{
|
||||
href: "/providers",
|
||||
label: "Providers",
|
||||
},
|
||||
{
|
||||
href: "/providers/taxjar",
|
||||
label: "TaxJar",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<TaxJarInstructions />
|
||||
<CreateTaxJarConfiguration />
|
||||
</AppColumns>
|
||||
</main>
|
||||
</AppPageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue