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 { AuthData } from "@saleor/app-sdk/APL";
|
||||||
import { OrderConfirmedSubscriptionFragment, TaxBaseFragment } from "../../../generated/graphql";
|
import { OrderConfirmedSubscriptionFragment } from "../../../generated/graphql";
|
||||||
import { Logger, createLogger } from "../../lib/logger";
|
import { Logger, createLogger } from "../../lib/logger";
|
||||||
|
import { CalculateTaxesPayload } from "../../pages/api/webhooks/checkout-calculate-taxes";
|
||||||
import { OrderCancelledPayload } from "../../pages/api/webhooks/order-cancelled";
|
import { OrderCancelledPayload } from "../../pages/api/webhooks/order-cancelled";
|
||||||
import { ProviderWebhookService } from "../taxes/tax-provider-webhook";
|
import { ProviderWebhookService } from "../taxes/tax-provider-webhook";
|
||||||
import { AvataxClient } from "./avatax-client";
|
import { AvataxConfig } from "./avatax-connection-schema";
|
||||||
import { AvataxConfig, defaultAvataxConfig } from "./avatax-connection-schema";
|
|
||||||
import { AvataxCalculateTaxesAdapter } from "./calculate-taxes/avatax-calculate-taxes-adapter";
|
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 { AvataxOrderCancelledAdapter } from "./order-cancelled/avatax-order-cancelled-adapter";
|
||||||
import { AvataxOrderConfirmedAdapter } from "./order-confirmed/avatax-order-confirmed-adapter";
|
import { AvataxOrderConfirmedAdapter } from "./order-confirmed/avatax-order-confirmed-adapter";
|
||||||
import { CalculateTaxesPayload } from "../../pages/api/webhooks/checkout-calculate-taxes";
|
|
||||||
|
|
||||||
export class AvataxWebhookService implements ProviderWebhookService {
|
export class AvataxWebhookService implements ProviderWebhookService {
|
||||||
config = defaultAvataxConfig;
|
|
||||||
client: AvataxClient;
|
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
private config: AvataxConfig;
|
||||||
|
private clientLogger: ClientLogger;
|
||||||
|
private authData: AuthData;
|
||||||
|
|
||||||
constructor(
|
constructor({
|
||||||
config: AvataxConfig,
|
config,
|
||||||
private authData: AuthData,
|
authData,
|
||||||
) {
|
clientLogger,
|
||||||
|
}: {
|
||||||
|
config: AvataxConfig;
|
||||||
|
authData: AuthData;
|
||||||
|
clientLogger: ClientLogger;
|
||||||
|
}) {
|
||||||
this.logger = createLogger({
|
this.logger = createLogger({
|
||||||
name: "AvataxWebhookService",
|
name: "AvataxWebhookService",
|
||||||
});
|
});
|
||||||
const avataxClient = new AvataxClient(config);
|
this.authData = authData;
|
||||||
|
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.client = avataxClient;
|
this.clientLogger = clientLogger;
|
||||||
}
|
}
|
||||||
|
|
||||||
async calculateTaxes(payload: CalculateTaxesPayload) {
|
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);
|
const response = await adapter.send(payload);
|
||||||
|
|
||||||
|
@ -37,7 +46,11 @@ export class AvataxWebhookService implements ProviderWebhookService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async confirmOrder(order: OrderConfirmedSubscriptionFragment) {
|
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 });
|
const response = await adapter.send({ order });
|
||||||
|
|
||||||
|
@ -45,7 +58,10 @@ export class AvataxWebhookService implements ProviderWebhookService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancelOrder(payload: OrderCancelledPayload) {
|
async cancelOrder(payload: OrderCancelledPayload) {
|
||||||
const adapter = new AvataxOrderCancelledAdapter(this.config);
|
const adapter = new AvataxOrderCancelledAdapter({
|
||||||
|
config: this.config,
|
||||||
|
clientLogger: this.clientLogger,
|
||||||
|
});
|
||||||
|
|
||||||
await adapter.send(payload);
|
await adapter.send(payload);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { CalculateTaxesResponse } from "../../taxes/tax-provider-webhook";
|
||||||
import { WebhookAdapter } from "../../taxes/tax-webhook-adapter";
|
import { WebhookAdapter } from "../../taxes/tax-webhook-adapter";
|
||||||
import { AvataxClient, CreateTransactionArgs } from "../avatax-client";
|
import { AvataxClient, CreateTransactionArgs } from "../avatax-client";
|
||||||
import { AvataxConfig } from "../avatax-connection-schema";
|
import { AvataxConfig } from "../avatax-connection-schema";
|
||||||
|
import { ClientLogger } from "../../logs/client-logger";
|
||||||
import { AvataxCalculateTaxesPayloadService } from "./avatax-calculate-taxes-payload.service";
|
import { AvataxCalculateTaxesPayloadService } from "./avatax-calculate-taxes-payload.service";
|
||||||
import { AvataxCalculateTaxesResponseTransformer } from "./avatax-calculate-taxes-response-transformer";
|
import { AvataxCalculateTaxesResponseTransformer } from "./avatax-calculate-taxes-response-transformer";
|
||||||
|
|
||||||
|
@ -17,33 +18,64 @@ export class AvataxCalculateTaxesAdapter
|
||||||
implements WebhookAdapter<CalculateTaxesPayload, AvataxCalculateTaxesResponse>
|
implements WebhookAdapter<CalculateTaxesPayload, AvataxCalculateTaxesResponse>
|
||||||
{
|
{
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
constructor(
|
private readonly config: AvataxConfig;
|
||||||
private readonly config: AvataxConfig,
|
private readonly authData: AuthData;
|
||||||
private authData: AuthData,
|
private readonly clientLogger: ClientLogger;
|
||||||
) {
|
|
||||||
|
constructor({
|
||||||
|
config,
|
||||||
|
authData,
|
||||||
|
clientLogger,
|
||||||
|
}: {
|
||||||
|
config: AvataxConfig;
|
||||||
|
clientLogger: ClientLogger;
|
||||||
|
authData: AuthData;
|
||||||
|
}) {
|
||||||
this.logger = createLogger({ name: "AvataxCalculateTaxesAdapter" });
|
this.logger = createLogger({ name: "AvataxCalculateTaxesAdapter" });
|
||||||
|
this.config = config;
|
||||||
|
this.clientLogger = clientLogger;
|
||||||
|
this.authData = authData;
|
||||||
}
|
}
|
||||||
|
|
||||||
async send(payload: CalculateTaxesPayload): Promise<AvataxCalculateTaxesResponse> {
|
async send(payload: CalculateTaxesPayload): Promise<AvataxCalculateTaxesResponse> {
|
||||||
this.logger.debug(
|
this.logger.debug("Transforming the Saleor payload for calculating taxes with AvaTax...");
|
||||||
{ payload },
|
|
||||||
"Transforming the Saleor payload for calculating taxes with AvaTax...",
|
|
||||||
);
|
|
||||||
const payloadService = new AvataxCalculateTaxesPayloadService(this.authData);
|
const payloadService = new AvataxCalculateTaxesPayloadService(this.authData);
|
||||||
const target = await payloadService.getPayload(payload, this.config);
|
const target = await payloadService.getPayload(payload, this.config);
|
||||||
|
|
||||||
this.logger.debug("Calling AvaTax createTransaction with transformed payload...");
|
this.logger.debug("Calling AvaTax createTransaction with transformed payload...");
|
||||||
|
|
||||||
const client = new AvataxClient(this.config);
|
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();
|
this.clientLogger.push({
|
||||||
const transformedResponse = responseTransformer.transform(response);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,15 +3,20 @@ import { OrderCancelledPayload } from "../../../pages/api/webhooks/order-cancell
|
||||||
import { WebhookAdapter } from "../../taxes/tax-webhook-adapter";
|
import { WebhookAdapter } from "../../taxes/tax-webhook-adapter";
|
||||||
import { AvataxClient, VoidTransactionArgs } from "../avatax-client";
|
import { AvataxClient, VoidTransactionArgs } from "../avatax-client";
|
||||||
import { AvataxConfig } from "../avatax-connection-schema";
|
import { AvataxConfig } from "../avatax-connection-schema";
|
||||||
|
import { ClientLogger } from "../../logs/client-logger";
|
||||||
import { AvataxOrderCancelledPayloadTransformer } from "./avatax-order-cancelled-payload-transformer";
|
import { AvataxOrderCancelledPayloadTransformer } from "./avatax-order-cancelled-payload-transformer";
|
||||||
|
|
||||||
export type AvataxOrderCancelledTarget = VoidTransactionArgs;
|
export type AvataxOrderCancelledTarget = VoidTransactionArgs;
|
||||||
|
|
||||||
export class AvataxOrderCancelledAdapter implements WebhookAdapter<OrderCancelledPayload, void> {
|
export class AvataxOrderCancelledAdapter implements WebhookAdapter<OrderCancelledPayload, void> {
|
||||||
private logger: Logger;
|
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.logger = createLogger({ name: "AvataxOrderCancelledAdapter" });
|
||||||
|
this.config = config;
|
||||||
|
this.clientLogger = clientLogger;
|
||||||
}
|
}
|
||||||
|
|
||||||
async send(payload: OrderCancelledPayload) {
|
async send(payload: OrderCancelledPayload) {
|
||||||
|
@ -24,8 +29,28 @@ export class AvataxOrderCancelledAdapter implements WebhookAdapter<OrderCancelle
|
||||||
|
|
||||||
const client = new AvataxClient(this.config);
|
const client = new AvataxClient(this.config);
|
||||||
|
|
||||||
await client.voidTransaction(target);
|
try {
|
||||||
|
const response = await client.voidTransaction(target);
|
||||||
|
|
||||||
this.logger.debug(`Successfully voided the transaction of id: ${target.transactionCode}`);
|
this.clientLogger.push({
|
||||||
|
event: "[OrderCancelled] voidTransaction",
|
||||||
|
status: "success",
|
||||||
|
payload: {
|
||||||
|
input: target,
|
||||||
|
output: response,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.debug(`Successfully voided the transaction of id: ${target.transactionCode}`);
|
||||||
|
} catch (error) {
|
||||||
|
this.clientLogger.push({
|
||||||
|
event: "[OrderCancelled] voidTransaction",
|
||||||
|
status: "error",
|
||||||
|
payload: {
|
||||||
|
input: target,
|
||||||
|
output: error,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { CreateOrderResponse } from "../../taxes/tax-provider-webhook";
|
||||||
import { WebhookAdapter } from "../../taxes/tax-webhook-adapter";
|
import { WebhookAdapter } from "../../taxes/tax-webhook-adapter";
|
||||||
import { AvataxClient } from "../avatax-client";
|
import { AvataxClient } from "../avatax-client";
|
||||||
import { AvataxConfig } from "../avatax-connection-schema";
|
import { AvataxConfig } from "../avatax-connection-schema";
|
||||||
|
import { ClientLogger } from "../../logs/client-logger";
|
||||||
import { AvataxOrderConfirmedPayloadService } from "./avatax-order-confirmed-payload.service";
|
import { AvataxOrderConfirmedPayloadService } from "./avatax-order-confirmed-payload.service";
|
||||||
import { AvataxOrderConfirmedResponseTransformer } from "./avatax-order-confirmed-response-transformer";
|
import { AvataxOrderConfirmedResponseTransformer } from "./avatax-order-confirmed-response-transformer";
|
||||||
|
|
||||||
|
@ -17,9 +18,23 @@ export class AvataxOrderConfirmedAdapter
|
||||||
implements WebhookAdapter<AvataxOrderConfirmedPayload, AvataxOrderConfirmedResponse>
|
implements WebhookAdapter<AvataxOrderConfirmedPayload, AvataxOrderConfirmedResponse>
|
||||||
{
|
{
|
||||||
private logger: Logger;
|
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.logger = createLogger({ name: "AvataxOrderConfirmedAdapter" });
|
||||||
|
this.config = config;
|
||||||
|
this.authData = authData;
|
||||||
|
this.clientLogger = clientLogger;
|
||||||
}
|
}
|
||||||
|
|
||||||
async send(payload: AvataxOrderConfirmedPayload): Promise<AvataxOrderConfirmedResponse> {
|
async send(payload: AvataxOrderConfirmedPayload): Promise<AvataxOrderConfirmedResponse> {
|
||||||
|
@ -31,15 +46,37 @@ export class AvataxOrderConfirmedAdapter
|
||||||
this.logger.debug("Calling AvaTax createTransaction with transformed payload...");
|
this.logger.debug("Calling AvaTax createTransaction with transformed payload...");
|
||||||
|
|
||||||
const client = new AvataxClient(this.config);
|
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();
|
this.clientLogger.push({
|
||||||
const transformedResponse = responseTransformer.transform(response);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
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 { AuthData } from "@saleor/app-sdk/APL";
|
||||||
import {
|
import { MetadataItem, OrderConfirmedSubscriptionFragment } from "../../../generated/graphql";
|
||||||
MetadataItem,
|
|
||||||
OrderConfirmedSubscriptionFragment,
|
|
||||||
TaxBaseFragment,
|
|
||||||
} from "../../../generated/graphql";
|
|
||||||
import { Logger, createLogger } from "../../lib/logger";
|
import { Logger, createLogger } from "../../lib/logger";
|
||||||
|
|
||||||
|
import { CalculateTaxesPayload } from "../../pages/api/webhooks/checkout-calculate-taxes";
|
||||||
import { OrderCancelledPayload } from "../../pages/api/webhooks/order-cancelled";
|
import { OrderCancelledPayload } from "../../pages/api/webhooks/order-cancelled";
|
||||||
import { getAppConfig } from "../app/get-app-config";
|
import { getAppConfig } from "../app/get-app-config";
|
||||||
import { AvataxWebhookService } from "../avatax/avatax-webhook.service";
|
import { AvataxWebhookService } from "../avatax/avatax-webhook.service";
|
||||||
import { ProviderConnection } from "../provider-connections/provider-connections";
|
import { ProviderConnection } from "../provider-connections/provider-connections";
|
||||||
import { TaxJarWebhookService } from "../taxjar/taxjar-webhook.service";
|
import { TaxJarWebhookService } from "../taxjar/taxjar-webhook.service";
|
||||||
import { ProviderWebhookService } from "./tax-provider-webhook";
|
import { ProviderWebhookService } from "./tax-provider-webhook";
|
||||||
import { CalculateTaxesPayload } from "../../pages/api/webhooks/checkout-calculate-taxes";
|
import { createClientLogger } from "../logs/client-logger";
|
||||||
|
|
||||||
// todo: refactor to a factory
|
// todo: refactor to a factory
|
||||||
class ActiveTaxProviderService implements ProviderWebhookService {
|
class ActiveTaxProviderService implements ProviderWebhookService {
|
||||||
|
@ -28,17 +25,29 @@ class ActiveTaxProviderService implements ProviderWebhookService {
|
||||||
});
|
});
|
||||||
|
|
||||||
const taxProviderName = providerConnection.provider;
|
const taxProviderName = providerConnection.provider;
|
||||||
|
const clientLogger = createClientLogger({
|
||||||
|
authData,
|
||||||
|
configurationId: providerConnection.id,
|
||||||
|
});
|
||||||
|
|
||||||
switch (taxProviderName) {
|
switch (taxProviderName) {
|
||||||
case "taxjar": {
|
case "taxjar": {
|
||||||
this.logger.debug("Selecting TaxJar as tax provider");
|
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;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case "avatax": {
|
case "avatax": {
|
||||||
this.logger.debug("Selecting AvaTax as tax provider");
|
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;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { FetchTaxForOrderArgs, TaxJarClient } from "../taxjar-client";
|
||||||
import { TaxJarConfig } from "../taxjar-connection-schema";
|
import { TaxJarConfig } from "../taxjar-connection-schema";
|
||||||
import { TaxJarCalculateTaxesPayloadService } from "./taxjar-calculate-taxes-payload-service";
|
import { TaxJarCalculateTaxesPayloadService } from "./taxjar-calculate-taxes-payload-service";
|
||||||
import { TaxJarCalculateTaxesResponseTransformer } from "./taxjar-calculate-taxes-response-transformer";
|
import { TaxJarCalculateTaxesResponseTransformer } from "./taxjar-calculate-taxes-response-transformer";
|
||||||
|
import { ClientLogger } from "../../logs/client-logger";
|
||||||
|
|
||||||
export type TaxJarCalculateTaxesPayload = {
|
export type TaxJarCalculateTaxesPayload = {
|
||||||
taxBase: TaxBaseFragment;
|
taxBase: TaxBaseFragment;
|
||||||
|
@ -19,8 +20,23 @@ export class TaxJarCalculateTaxesAdapter
|
||||||
implements WebhookAdapter<TaxJarCalculateTaxesPayload, TaxJarCalculateTaxesResponse>
|
implements WebhookAdapter<TaxJarCalculateTaxesPayload, TaxJarCalculateTaxesResponse>
|
||||||
{
|
{
|
||||||
private logger: Logger;
|
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.logger = createLogger({ name: "TaxJarCalculateTaxesAdapter" });
|
||||||
|
this.clientLogger = clientLogger;
|
||||||
|
this.authData = authData;
|
||||||
|
this.config = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo: refactor because its getting too big
|
// todo: refactor because its getting too big
|
||||||
|
@ -32,15 +48,37 @@ export class TaxJarCalculateTaxesAdapter
|
||||||
this.logger.debug("Calling TaxJar fetchTaxForOrder with transformed payload...");
|
this.logger.debug("Calling TaxJar fetchTaxForOrder with transformed payload...");
|
||||||
|
|
||||||
const client = new TaxJarClient(this.config);
|
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();
|
this.clientLogger.push({
|
||||||
const transformedResponse = responseTransformer.transform(payload, response);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,18 @@
|
||||||
|
import { Logger, createLogger } from "../../../lib/logger";
|
||||||
import { taxJarAddressFactory } from "../address-factory";
|
import { taxJarAddressFactory } from "../address-factory";
|
||||||
import { TaxJarClient } from "../taxjar-client";
|
import { TaxJarClient } from "../taxjar-client";
|
||||||
import { TaxJarConfig } from "../taxjar-connection-schema";
|
import { TaxJarConfig } from "../taxjar-connection-schema";
|
||||||
import { TaxJarValidationErrorResolver } from "./tax-jar-validation-error-resolver";
|
import { TaxJarValidationErrorResolver } from "./tax-jar-validation-error-resolver";
|
||||||
|
|
||||||
export class TaxJarValidationService {
|
export class TaxJarValidationService {
|
||||||
|
private logger: Logger;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.logger = createLogger({
|
||||||
|
name: "TaxJarValidationService",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async validate(config: TaxJarConfig): Promise<void> {
|
async validate(config: TaxJarConfig): Promise<void> {
|
||||||
const taxJarClient = new TaxJarClient(config);
|
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)
|
// if the address is invalid, TaxJar will throw an error (rather than 200 and error messages)
|
||||||
await taxJarClient.validateAddress({ params: address });
|
await taxJarClient.validateAddress({ params: address });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
this.logger.debug({ error });
|
||||||
const errorResolver = new TaxJarValidationErrorResolver();
|
const errorResolver = new TaxJarValidationErrorResolver();
|
||||||
|
|
||||||
throw errorResolver.resolve(error);
|
throw errorResolver.resolve(error);
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { CreateOrderArgs, TaxJarClient } from "../taxjar-client";
|
||||||
import { TaxJarConfig } from "../taxjar-connection-schema";
|
import { TaxJarConfig } from "../taxjar-connection-schema";
|
||||||
import { TaxJarOrderConfirmedPayloadService } from "./taxjar-order-confirmed-payload.service";
|
import { TaxJarOrderConfirmedPayloadService } from "./taxjar-order-confirmed-payload.service";
|
||||||
import { TaxJarOrderConfirmedResponseTransformer } from "./taxjar-order-confirmed-response-transformer";
|
import { TaxJarOrderConfirmedResponseTransformer } from "./taxjar-order-confirmed-response-transformer";
|
||||||
|
import { ClientLogger } from "../../logs/client-logger";
|
||||||
|
|
||||||
export type TaxJarOrderConfirmedPayload = {
|
export type TaxJarOrderConfirmedPayload = {
|
||||||
order: OrderConfirmedSubscriptionFragment;
|
order: OrderConfirmedSubscriptionFragment;
|
||||||
|
@ -18,7 +19,22 @@ export class TaxJarOrderConfirmedAdapter
|
||||||
implements WebhookAdapter<TaxJarOrderConfirmedPayload, TaxJarOrderConfirmedResponse>
|
implements WebhookAdapter<TaxJarOrderConfirmedPayload, TaxJarOrderConfirmedResponse>
|
||||||
{
|
{
|
||||||
private logger: Logger;
|
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" });
|
this.logger = createLogger({ name: "TaxJarOrderConfirmedAdapter" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,14 +46,36 @@ export class TaxJarOrderConfirmedAdapter
|
||||||
this.logger.debug("Calling TaxJar fetchTaxForOrder with transformed payload...");
|
this.logger.debug("Calling TaxJar fetchTaxForOrder with transformed payload...");
|
||||||
|
|
||||||
const client = new TaxJarClient(this.config);
|
const client = new TaxJarClient(this.config);
|
||||||
const response = await client.createOrder(target);
|
|
||||||
|
|
||||||
this.logger.debug("TaxJar createOrder successfully responded");
|
try {
|
||||||
const responseTransformer = new TaxJarOrderConfirmedResponseTransformer();
|
const response = await client.createOrder(target);
|
||||||
const transformedResponse = responseTransformer.transform(response);
|
|
||||||
|
|
||||||
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
* @see https://github.com/taxjar/taxjar-node/issues/70
|
||||||
*/
|
*/
|
||||||
async validateAddress({ params }: ValidateAddressArgs) {
|
async validateAddress({ params }: ValidateAddressArgs) {
|
||||||
|
|
|
@ -2,36 +2,50 @@ import { AuthData } from "@saleor/app-sdk/APL";
|
||||||
import {
|
import {
|
||||||
OrderCancelledEventSubscriptionFragment,
|
OrderCancelledEventSubscriptionFragment,
|
||||||
OrderConfirmedSubscriptionFragment,
|
OrderConfirmedSubscriptionFragment,
|
||||||
TaxBaseFragment,
|
|
||||||
} from "../../../generated/graphql";
|
} from "../../../generated/graphql";
|
||||||
import { Logger, createLogger } from "../../lib/logger";
|
import { Logger, createLogger } from "../../lib/logger";
|
||||||
|
import { CalculateTaxesPayload } from "../../pages/api/webhooks/checkout-calculate-taxes";
|
||||||
import { ProviderWebhookService } from "../taxes/tax-provider-webhook";
|
import { ProviderWebhookService } from "../taxes/tax-provider-webhook";
|
||||||
import { TaxJarCalculateTaxesAdapter } from "./calculate-taxes/taxjar-calculate-taxes-adapter";
|
import { TaxJarCalculateTaxesAdapter } from "./calculate-taxes/taxjar-calculate-taxes-adapter";
|
||||||
import { TaxJarOrderConfirmedAdapter } from "./order-confirmed/taxjar-order-confirmed-adapter";
|
import { TaxJarOrderConfirmedAdapter } from "./order-confirmed/taxjar-order-confirmed-adapter";
|
||||||
import { TaxJarClient } from "./taxjar-client";
|
import { TaxJarClient } from "./taxjar-client";
|
||||||
import { TaxJarConfig } from "./taxjar-connection-schema";
|
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 {
|
export class TaxJarWebhookService implements ProviderWebhookService {
|
||||||
client: TaxJarClient;
|
client: TaxJarClient;
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
private config: TaxJarConfig;
|
private config: TaxJarConfig;
|
||||||
|
private clientLogger: ClientLogger;
|
||||||
|
private authData: AuthData;
|
||||||
|
|
||||||
constructor(
|
constructor({
|
||||||
config: TaxJarConfig,
|
clientLogger,
|
||||||
private authData: AuthData,
|
config,
|
||||||
) {
|
authData,
|
||||||
|
}: {
|
||||||
|
clientLogger: ClientLogger;
|
||||||
|
config: TaxJarConfig;
|
||||||
|
authData: AuthData;
|
||||||
|
}) {
|
||||||
const taxJarClient = new TaxJarClient(config);
|
const taxJarClient = new TaxJarClient(config);
|
||||||
|
|
||||||
this.client = taxJarClient;
|
this.client = taxJarClient;
|
||||||
this.config = config;
|
this.config = config;
|
||||||
|
this.authData = authData;
|
||||||
|
this.clientLogger = clientLogger;
|
||||||
|
|
||||||
this.logger = createLogger({
|
this.logger = createLogger({
|
||||||
name: "TaxJarWebhookService",
|
name: "TaxJarWebhookService",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async calculateTaxes(payload: CalculateTaxesPayload) {
|
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);
|
const response = await adapter.send(payload);
|
||||||
|
|
||||||
|
@ -39,7 +53,11 @@ export class TaxJarWebhookService implements ProviderWebhookService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async confirmOrder(order: OrderConfirmedSubscriptionFragment) {
|
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 });
|
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 { 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 { 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.:
|
* // todo: split to namespaces, e.g.:
|
||||||
|
@ -23,6 +24,7 @@ export const appRouter = router({
|
||||||
taxJarTaxCodes: taxJarTaxCodesRouter,
|
taxJarTaxCodes: taxJarTaxCodesRouter,
|
||||||
taxJarMatches: taxJarTaxCodeMatchesRouter,
|
taxJarMatches: taxJarTaxCodeMatchesRouter,
|
||||||
avataxMatches: avataxTaxCodeMatchesRouter,
|
avataxMatches: avataxTaxCodeMatchesRouter,
|
||||||
|
clientLogs: clientLoggerRouter,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type AppRouter = typeof appRouter;
|
export type AppRouter = typeof appRouter;
|
||||||
|
|
|
@ -1,97 +1,11 @@
|
||||||
import { Breadcrumbs } from "@saleor/apps-ui";
|
import { Breadcrumbs } from "@saleor/apps-ui";
|
||||||
import { useRouter } from "next/router";
|
|
||||||
|
|
||||||
type Breadcrumb = {
|
export type Breadcrumb = {
|
||||||
label: string;
|
label: string;
|
||||||
href?: string;
|
href?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const newProviderBreadcrumbs: Breadcrumb[] = [
|
export const AppBreadcrumbs = ({ breadcrumbs }: { breadcrumbs: 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();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
{breadcrumbs.map((breadcrumb) => (
|
{breadcrumbs.map((breadcrumb) => (
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
import { Box } from "@saleor/macaw-ui/next";
|
import { Box } from "@saleor/macaw-ui/next";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { AppBreadcrumbs } from "./app-breadcrumbs";
|
|
||||||
|
|
||||||
export const AppLayout = ({ children }: { children: React.ReactNode }) => {
|
export const AppLayout = ({ children }: { children: React.ReactNode }) => {
|
||||||
return (
|
return (
|
||||||
<Box display={"flex"} flexDirection={"column"} gap={8} paddingY={4}>
|
<Box display={"flex"} flexDirection={"column"} gap={8} paddingY={4}>
|
||||||
<AppBreadcrumbs />
|
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</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,
|
description,
|
||||||
...props
|
...props
|
||||||
}: PropsWithBox<{
|
}: PropsWithBox<{
|
||||||
title: React.ReactNode;
|
title?: React.ReactNode;
|
||||||
description: React.ReactNode;
|
description?: React.ReactNode;
|
||||||
}>) => {
|
}>) => {
|
||||||
return (
|
return (
|
||||||
<Box display="flex" flexDirection={"column"} gap={10} __maxWidth={MAX_WIDTH} {...props}>
|
<Box display="flex" flexDirection={"column"} gap={10} __maxWidth={MAX_WIDTH} {...props}>
|
||||||
<Text as="h3" variant="heading">
|
{title && (
|
||||||
{title}
|
<Text as="h3" variant="heading">
|
||||||
</Text>
|
{title}
|
||||||
<Box fontWeight={"bodyMedium"} fontSize={"bodyMedium"}>
|
</Text>
|
||||||
{description}
|
)}
|
||||||
</Box>
|
{description && (
|
||||||
|
<Box fontWeight={"bodyMedium"} fontSize={"bodyMedium"}>
|
||||||
|
{description}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</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 { useRouter } from "next/router";
|
||||||
import { ProviderConnection } from "../provider-connections/provider-connections";
|
import { ProviderConnection } from "../provider-connections/provider-connections";
|
||||||
import { trpcClient } from "../trpc/trpc-client";
|
import { trpcClient } from "../trpc/trpc-client";
|
||||||
|
@ -9,10 +9,14 @@ export const ProvidersTable = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { data } = trpcClient.providersConfiguration.getAll.useQuery();
|
const { data } = trpcClient.providersConfiguration.getAll.useQuery();
|
||||||
|
|
||||||
const itemClickHandler = (item: ProviderConnection) => {
|
const editClickHandler = (item: ProviderConnection) => {
|
||||||
router.push(`/providers/${item.provider}/${item.id}`);
|
router.push(`/providers/${item.provider}/${item.id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const logsClickHandler = (item: ProviderConnection) => {
|
||||||
|
router.push(`/providers/${item.provider}/${item.id}/logs`);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table.Container>
|
<Table.Container>
|
||||||
<Table.THead color={"textNeutralSubdued"}>
|
<Table.THead color={"textNeutralSubdued"}>
|
||||||
|
@ -28,9 +32,22 @@ export const ProvidersTable = () => {
|
||||||
<Table.TD>
|
<Table.TD>
|
||||||
<ProviderLabel name={item.provider} />
|
<ProviderLabel name={item.provider} />
|
||||||
</Table.TD>
|
</Table.TD>
|
||||||
<Table.TD onClick={() => itemClickHandler(item)}>
|
<Table.TD>
|
||||||
<Box display={"flex"} justifyContent={"flex-end"}>
|
<Box display={"flex"} justifyContent={"flex-end"} gap={2}>
|
||||||
<Button data-testid="provider-edit-button" variant="tertiary">
|
<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
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</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 = {
|
export const Table = {
|
||||||
Container: (props: BoxProps) => (
|
Container: (props: BoxProps) => (
|
||||||
|
@ -11,4 +34,5 @@ export const Table = {
|
||||||
),
|
),
|
||||||
TBody: (props: BoxProps) => <Box {...props} as="tbody" />,
|
TBody: (props: BoxProps) => <Box {...props} as="tbody" />,
|
||||||
TD: (props: BoxProps) => <Box fontSize="bodyMedium" paddingTop={2} {...props} as="td" />,
|
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) {
|
function verifyCalculateTaxesPayload(payload: CalculateTaxesPayload) {
|
||||||
if (!payload.taxBase.lines) {
|
if (!payload.taxBase.lines.length) {
|
||||||
throw new Error("No lines found in taxBase");
|
throw new Error("No lines found in taxBase");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,7 +48,7 @@ export default checkoutCalculateTaxesSyncWebhook.createHandler(async (req, res,
|
||||||
|
|
||||||
try {
|
try {
|
||||||
verifyCalculateTaxesPayload(payload);
|
verifyCalculateTaxesPayload(payload);
|
||||||
logger.debug("Payload validated succesfully");
|
logger.debug("Payload validated Successfully");
|
||||||
|
|
||||||
const appMetadata = payload.recipient?.privateMetadata ?? [];
|
const appMetadata = payload.recipient?.privateMetadata ?? [];
|
||||||
const channelSlug = payload.taxBase.channel.slug;
|
const channelSlug = payload.taxBase.channel.slug;
|
||||||
|
|
|
@ -17,7 +17,7 @@ export const config = {
|
||||||
type CalculateTaxesPayload = Extract<CalculateTaxesEventFragment, { __typename: "CalculateTaxes" }>;
|
type CalculateTaxesPayload = Extract<CalculateTaxesEventFragment, { __typename: "CalculateTaxes" }>;
|
||||||
|
|
||||||
function verifyCalculateTaxesPayload(payload: CalculateTaxesPayload) {
|
function verifyCalculateTaxesPayload(payload: CalculateTaxesPayload) {
|
||||||
if (!payload.taxBase.lines) {
|
if (!payload.taxBase.lines.length) {
|
||||||
throw new Error("No lines found in taxBase");
|
throw new Error("No lines found in taxBase");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { ChannelSection } from "../modules/channel-configuration/ui/channel-section";
|
import { ChannelSection } from "../modules/channel-configuration/ui/channel-section";
|
||||||
import { ProvidersSection } from "../modules/provider-connections/ui/providers-section";
|
import { ProvidersSection } from "../modules/provider-connections/ui/providers-section";
|
||||||
import { AppColumns } from "../modules/ui/app-columns";
|
import { AppColumns } from "../modules/ui/app-columns";
|
||||||
|
import { AppPageLayout } from "../modules/ui/app-page-layout";
|
||||||
import { Section } from "../modules/ui/app-section";
|
import { Section } from "../modules/ui/app-section";
|
||||||
import { MatcherSection } from "../modules/ui/matcher-section";
|
import { MatcherSection } from "../modules/ui/matcher-section";
|
||||||
|
|
||||||
|
@ -14,11 +15,19 @@ const Header = () => {
|
||||||
|
|
||||||
const ConfigurationPage = () => {
|
const ConfigurationPage = () => {
|
||||||
return (
|
return (
|
||||||
<AppColumns top={<Header />}>
|
<AppPageLayout
|
||||||
|
top={<Header />}
|
||||||
|
breadcrumbs={[
|
||||||
|
{
|
||||||
|
href: "/configuration",
|
||||||
|
label: "Configuration",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
<ProvidersSection />
|
<ProvidersSection />
|
||||||
<ChannelSection />
|
<ChannelSection />
|
||||||
<MatcherSection />
|
<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 { Provider } from "jotai";
|
||||||
import { AvataxInstructions } from "../../../modules/avatax/ui/avatax-instructions";
|
import { AvataxInstructions } from "../../../modules/avatax/ui/avatax-instructions";
|
||||||
import { CreateAvataxConfiguration } from "../../../modules/avatax/ui/create-avatax-configuration";
|
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 = () => {
|
const Header = () => {
|
||||||
return (
|
return (
|
||||||
|
@ -16,14 +16,28 @@ const Header = () => {
|
||||||
|
|
||||||
const NewAvataxPage = () => {
|
const NewAvataxPage = () => {
|
||||||
return (
|
return (
|
||||||
<main>
|
<AppPageLayout
|
||||||
<AppColumns top={<Header />}>
|
breadcrumbs={[
|
||||||
<AvataxInstructions />
|
{
|
||||||
<Provider>
|
href: "/configuration",
|
||||||
<CreateAvataxConfiguration />
|
label: "Configuration",
|
||||||
</Provider>
|
},
|
||||||
</AppColumns>
|
{
|
||||||
</main>
|
href: "/providers",
|
||||||
|
label: "Providers",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: "/providers/avatax",
|
||||||
|
label: "AvaTax",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
top={<Header />}
|
||||||
|
>
|
||||||
|
<AvataxInstructions />
|
||||||
|
<Provider>
|
||||||
|
<CreateAvataxConfiguration />
|
||||||
|
</Provider>
|
||||||
|
</AppPageLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { AppCard } from "../../modules/ui/app-card";
|
||||||
import { AppColumns } from "../../modules/ui/app-columns";
|
import { AppColumns } from "../../modules/ui/app-columns";
|
||||||
import { ProviderLabel } from "../../modules/ui/provider-label";
|
import { ProviderLabel } from "../../modules/ui/provider-label";
|
||||||
import { Section } from "../../modules/ui/app-section";
|
import { Section } from "../../modules/ui/app-section";
|
||||||
|
import { AppPageLayout } from "../../modules/ui/app-page-layout";
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
return (
|
return (
|
||||||
|
@ -107,12 +108,22 @@ const ChooseProvider = () => {
|
||||||
|
|
||||||
const NewProviderPage = () => {
|
const NewProviderPage = () => {
|
||||||
return (
|
return (
|
||||||
<main>
|
<AppPageLayout
|
||||||
<AppColumns top={<Header />}>
|
breadcrumbs={[
|
||||||
<Intro />
|
{
|
||||||
<ChooseProvider />
|
href: "/configuration",
|
||||||
</AppColumns>
|
label: "Configuration",
|
||||||
</main>
|
},
|
||||||
|
{
|
||||||
|
href: "/providers",
|
||||||
|
label: "Providers",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
top={<Header />}
|
||||||
|
>
|
||||||
|
<Intro />
|
||||||
|
<ChooseProvider />
|
||||||
|
</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 { Box, Text } from "@saleor/macaw-ui/next";
|
||||||
import { AppColumns } from "../../../modules/ui/app-columns";
|
|
||||||
import { CreateTaxJarConfiguration } from "../../../modules/taxjar/ui/create-taxjar-configuration";
|
import { CreateTaxJarConfiguration } from "../../../modules/taxjar/ui/create-taxjar-configuration";
|
||||||
import { TaxJarInstructions } from "../../../modules/taxjar/ui/taxjar-instructions";
|
import { TaxJarInstructions } from "../../../modules/taxjar/ui/taxjar-instructions";
|
||||||
|
import { AppPageLayout } from "../../../modules/ui/app-page-layout";
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
return (
|
return (
|
||||||
|
@ -15,12 +15,26 @@ const Header = () => {
|
||||||
|
|
||||||
const NewTaxJarPage = () => {
|
const NewTaxJarPage = () => {
|
||||||
return (
|
return (
|
||||||
<main>
|
<AppPageLayout
|
||||||
<AppColumns top={<Header />}>
|
top={<Header />}
|
||||||
<TaxJarInstructions />
|
breadcrumbs={[
|
||||||
<CreateTaxJarConfiguration />
|
{
|
||||||
</AppColumns>
|
href: "/configuration",
|
||||||
</main>
|
label: "Configuration",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: "/providers",
|
||||||
|
label: "Providers",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: "/providers/taxjar",
|
||||||
|
label: "TaxJar",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<TaxJarInstructions />
|
||||||
|
<CreateTaxJarConfiguration />
|
||||||
|
</AppPageLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue