Merge pull request #244 from saleor/refactor/use-sync-webhook
refactor/use sync-webhook
This commit is contained in:
commit
7632c44ab0
16 changed files with 181 additions and 360 deletions
5
.changeset/real-tigers-fry.md
Normal file
5
.changeset/real-tigers-fry.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"saleor-app-taxes": patch
|
||||
---
|
||||
|
||||
Removed the draft implementation of the SaleorSyncWebhook. Replaced it with SaleorSyncWebhook class from app-sdk and bumped it to 0.34.1.
|
|
@ -19,7 +19,7 @@
|
|||
"@material-ui/core": "^4.12.4",
|
||||
"@material-ui/icons": "^4.11.3",
|
||||
"@material-ui/lab": "4.0.0-alpha.61",
|
||||
"@saleor/app-sdk": "0.33.0",
|
||||
"@saleor/app-sdk": "0.34.1",
|
||||
"@saleor/apps-shared": "workspace:*",
|
||||
"@saleor/macaw-ui": "^0.7.2",
|
||||
"@tanstack/react-query": "^4.19.1",
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
export * from "./saleor-app-sdk-copy";
|
||||
export * from "./saleor-sync-webhook";
|
||||
export * from "./saleor-webhook";
|
|
@ -1,49 +0,0 @@
|
|||
import { APL, AuthData } from "@saleor/app-sdk/APL";
|
||||
import { SyncWebhookEventType } from "@saleor/app-sdk/types";
|
||||
import { ASTNode, print } from "graphql";
|
||||
|
||||
// ! start: borrowed from saleor-app-sdk
|
||||
interface WebhookManifestConfigurationBase {
|
||||
name?: string;
|
||||
webhookPath: string;
|
||||
syncEvent: SyncWebhookEventType;
|
||||
isActive?: boolean;
|
||||
apl: APL;
|
||||
}
|
||||
|
||||
interface WebhookManifestConfigurationWithAst extends WebhookManifestConfigurationBase {
|
||||
subscriptionQueryAst: ASTNode;
|
||||
}
|
||||
|
||||
interface WebhookManifestConfigurationWithQuery extends WebhookManifestConfigurationBase {
|
||||
query: string;
|
||||
}
|
||||
|
||||
export type WebhookManifestConfiguration =
|
||||
| WebhookManifestConfigurationWithAst
|
||||
| WebhookManifestConfigurationWithQuery;
|
||||
|
||||
export const gqlAstToString = (ast: ASTNode) =>
|
||||
print(ast) // convert AST to string
|
||||
.replaceAll(/\n*/g, "") // remove new lines
|
||||
.replaceAll(/\s{2,}/g, " ") // remove unnecessary multiple spaces
|
||||
.trim(); // remove whitespace from beginning and end
|
||||
|
||||
export type WebhookContext<T> = {
|
||||
baseUrl: string;
|
||||
event: string;
|
||||
payload: T;
|
||||
authData: AuthData;
|
||||
};
|
||||
|
||||
export const toStringOrUndefined = (value: string | string[] | undefined) =>
|
||||
value ? value.toString() : undefined;
|
||||
|
||||
export const SALEOR_API_URL_HEADER = "saleor-api-url";
|
||||
|
||||
export const getBaseUrl = (headers: { [name: string]: string | string[] | undefined }): string => {
|
||||
const { host, "x-forwarded-proto": protocol = "http" } = headers;
|
||||
return `${protocol}://${host}`;
|
||||
};
|
||||
|
||||
// ! end: borrowed from saleor-app-sdk
|
|
@ -1,89 +0,0 @@
|
|||
import { APL } from "@saleor/app-sdk/APL";
|
||||
import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next";
|
||||
import { SyncWebhookEventType, WebhookManifest } from "@saleor/app-sdk/types";
|
||||
import { ASTNode } from "graphql";
|
||||
import { NextApiHandler } from "next";
|
||||
// todo: move to app-sdk
|
||||
import {
|
||||
getBaseUrl,
|
||||
gqlAstToString,
|
||||
SALEOR_API_URL_HEADER,
|
||||
toStringOrUndefined,
|
||||
WebhookContext,
|
||||
WebhookManifestConfiguration,
|
||||
} from "./saleor-app-sdk-copy";
|
||||
import { SaleorWebhook } from "./saleor-webhook";
|
||||
|
||||
export class SaleorSyncWebhook<TPayload = any, TResponse = any>
|
||||
implements SaleorWebhook<TPayload, TResponse>
|
||||
{
|
||||
name: string;
|
||||
webhookPath: string;
|
||||
event: SyncWebhookEventType;
|
||||
apl: APL;
|
||||
subscriptionQueryAst?: ASTNode;
|
||||
query?: string;
|
||||
isActive?: boolean;
|
||||
constructor(configuration: WebhookManifestConfiguration) {
|
||||
const { name, webhookPath, syncEvent, apl, isActive = true } = configuration;
|
||||
this.name = name || `${syncEvent} webhook`;
|
||||
if ("query" in configuration) {
|
||||
this.query = configuration.query;
|
||||
}
|
||||
if ("subscriptionQueryAst" in configuration) {
|
||||
this.subscriptionQueryAst = configuration.subscriptionQueryAst;
|
||||
}
|
||||
if (!this.subscriptionQueryAst && !this.query) {
|
||||
// todo: replace with WebhookError in sdk
|
||||
throw new Error(
|
||||
"Need to specify `subscriptionQueryAst` or `query` to create webhook subscription"
|
||||
);
|
||||
}
|
||||
|
||||
this.webhookPath = webhookPath;
|
||||
this.event = syncEvent;
|
||||
this.isActive = isActive;
|
||||
this.apl = apl;
|
||||
}
|
||||
getTargetUrl(baseUrl: string): string {
|
||||
return new URL(this.webhookPath, baseUrl).href;
|
||||
}
|
||||
getWebhookManifest(baseUrl: string): WebhookManifest {
|
||||
return {
|
||||
name: this.name,
|
||||
targetUrl: this.getTargetUrl(baseUrl),
|
||||
syncEvents: [this.event],
|
||||
isActive: this.isActive,
|
||||
// Query can be provided as plaintext..
|
||||
...(this.query && { query: this.query }),
|
||||
// ...GQL AST which has to be stringified..
|
||||
...(this.subscriptionQueryAst && { query: gqlAstToString(this.subscriptionQueryAst) }),
|
||||
// or no query at all. In such case default webhook payload will be sent
|
||||
};
|
||||
}
|
||||
createHandler(handlerFn: NextWebhookApiHandler<TPayload, TResponse>): NextApiHandler<TResponse> {
|
||||
return async (req, res) => {
|
||||
const saleorApiUrl = toStringOrUndefined(req.headers[SALEOR_API_URL_HEADER]);
|
||||
|
||||
if (!saleorApiUrl) {
|
||||
return res.status(400).end();
|
||||
}
|
||||
|
||||
const authData = await this.apl.get(saleorApiUrl);
|
||||
|
||||
if (!authData) {
|
||||
return res.status(401).end();
|
||||
}
|
||||
|
||||
const baseUrl = getBaseUrl(req.headers);
|
||||
|
||||
const context: WebhookContext<TPayload> = {
|
||||
authData,
|
||||
baseUrl,
|
||||
event: this.event,
|
||||
payload: req.body,
|
||||
};
|
||||
return handlerFn(req, res, context);
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
import { APL } from "@saleor/app-sdk/APL";
|
||||
import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next";
|
||||
import { SyncWebhookEventType, WebhookManifest } from "@saleor/app-sdk/types";
|
||||
import { ASTNode } from "graphql";
|
||||
import { NextApiHandler } from "next";
|
||||
|
||||
export interface SaleorWebhook<TPayload = any, TResponse = any> {
|
||||
name: string;
|
||||
webhookPath: string;
|
||||
event: SyncWebhookEventType;
|
||||
apl: APL;
|
||||
subscriptionQueryAst?: ASTNode;
|
||||
query?: string;
|
||||
isActive?: boolean;
|
||||
|
||||
getTargetUrl(baseUrl: string): string;
|
||||
getWebhookManifest(baseUrl: string): WebhookManifest;
|
||||
createHandler(handlerFn: NextWebhookApiHandler<TPayload, TResponse>): NextApiHandler<TResponse>;
|
||||
}
|
|
@ -1,104 +0,0 @@
|
|||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { ZodError } from "zod";
|
||||
import { CalculateTaxes } from "../../../generated/graphql";
|
||||
import { ChannelConfig } from "../../modules/channels-configuration/channels-config";
|
||||
import { GetChannelsConfigurationService } from "../../modules/channels-configuration/get-channels-configuration.service";
|
||||
import { GetProvidersConfigurationService } from "../../modules/providers-configuration/get-providers-configuration.service";
|
||||
import { ProviderConfig } from "../../modules/providers-configuration/providers-config";
|
||||
import { defaultTaxesResponse } from "../../modules/taxes/defaults";
|
||||
import { TaxProviderError } from "../../modules/taxes/tax-provider-error";
|
||||
import { createClient } from "../graphql";
|
||||
import { createLogger } from "../logger";
|
||||
import { WebhookContext } from "./saleor-app-sdk";
|
||||
import { calculateTaxesPayloadSchema, ExpectedWebhookPayload } from "./schema";
|
||||
import { ResponseTaxPayload } from "../../modules/taxes/types";
|
||||
|
||||
export const withTaxesWebhook =
|
||||
(
|
||||
handler: (
|
||||
payload: ExpectedWebhookPayload,
|
||||
config: {
|
||||
provider: ProviderConfig;
|
||||
channel: ChannelConfig;
|
||||
},
|
||||
response: NextApiResponse<ResponseTaxPayload>
|
||||
) => Promise<void>
|
||||
) =>
|
||||
async (req: NextApiRequest, res: NextApiResponse, context: WebhookContext<CalculateTaxes>) => {
|
||||
const logger = createLogger({ event: context.event });
|
||||
const { authData, payload } = context;
|
||||
|
||||
logger.info("Webhook triggered. withTaxesWebhook called");
|
||||
logger.info({ payload }, "Payload received");
|
||||
|
||||
if (!authData) {
|
||||
logger.error("Auth data not found");
|
||||
logger.info(defaultTaxesResponse, "Responding with the defaultTaxesResponse");
|
||||
return res.status(200).json(defaultTaxesResponse);
|
||||
}
|
||||
|
||||
logger.info("Parsing payload...");
|
||||
|
||||
const validation = calculateTaxesPayloadSchema.safeParse(req.body);
|
||||
|
||||
if (!validation.success) {
|
||||
logger.error({ error: validation.error.message }, "Payload is invalid");
|
||||
logger.info(defaultTaxesResponse, "Responding with the defaultTaxesResponse");
|
||||
return res.status(200).json(defaultTaxesResponse);
|
||||
}
|
||||
|
||||
const { data } = validation;
|
||||
logger.info({ data }, "Payload is valid.");
|
||||
|
||||
try {
|
||||
const { authData } = context;
|
||||
const client = createClient(authData.saleorApiUrl, async () =>
|
||||
Promise.resolve({ token: authData.token })
|
||||
);
|
||||
const providersConfig = await new GetProvidersConfigurationService({
|
||||
saleorApiUrl: authData.saleorApiUrl,
|
||||
apiClient: client,
|
||||
}).getConfiguration();
|
||||
|
||||
const channelsConfig = await new GetChannelsConfigurationService({
|
||||
saleorApiUrl: authData.saleorApiUrl,
|
||||
apiClient: client,
|
||||
}).getConfiguration();
|
||||
|
||||
logger.info({ providersConfig }, "Providers configuration returned");
|
||||
|
||||
const channelSlug = payload.taxBase.channel.slug;
|
||||
const channelConfig = channelsConfig[channelSlug];
|
||||
|
||||
if (!channelConfig) {
|
||||
logger.error(`Channel config not found for channel ${channelSlug}`);
|
||||
logger.info(defaultTaxesResponse, "Responding with the defaultTaxesResponse");
|
||||
return res.status(200).json(defaultTaxesResponse);
|
||||
}
|
||||
|
||||
const providerInstance = providersConfig.find(
|
||||
(instance) => instance.id === channelConfig.providerInstanceId
|
||||
);
|
||||
|
||||
if (!providerInstance) {
|
||||
logger.error(`Channel (${channelSlug}) providerInstanceId does not match any providers`);
|
||||
logger.info(defaultTaxesResponse, "Responding with the defaultTaxesResponse");
|
||||
return res.status(200).json(defaultTaxesResponse);
|
||||
}
|
||||
|
||||
return handler(data, { provider: providerInstance, channel: channelConfig }, res);
|
||||
} catch (error) {
|
||||
// todo: improve error handling; currently instanceof zod is not working
|
||||
if (error instanceof ZodError) {
|
||||
logger.error({ message: error.message }, "Zod error");
|
||||
}
|
||||
if (error instanceof TaxProviderError) {
|
||||
logger.error({ error }, "TaxProviderError");
|
||||
} else {
|
||||
logger.error({ error }, "Unknown error");
|
||||
}
|
||||
|
||||
logger.info(defaultTaxesResponse, "Responding with the defaultTaxesResponse");
|
||||
return res.status(200).json(defaultTaxesResponse);
|
||||
}
|
||||
};
|
|
@ -1,8 +0,0 @@
|
|||
import { ResponseTaxPayload } from "./types";
|
||||
|
||||
export const defaultTaxesResponse: ResponseTaxPayload = {
|
||||
lines: [],
|
||||
shipping_price_gross_amount: 0,
|
||||
shipping_price_net_amount: 0,
|
||||
shipping_tax_rate: "0",
|
||||
};
|
|
@ -1,19 +0,0 @@
|
|||
import { ResponseTaxPayload } from "./types";
|
||||
|
||||
export const MOCKED_RESPONSE_TAX_PAYLOAD: ResponseTaxPayload = {
|
||||
lines: [
|
||||
{
|
||||
tax_rate: "10",
|
||||
total_gross_amount: 4,
|
||||
total_net_amount: 3,
|
||||
},
|
||||
{
|
||||
tax_rate: "10",
|
||||
total_gross_amount: 20,
|
||||
total_net_amount: 5,
|
||||
},
|
||||
],
|
||||
shipping_price_gross_amount: 0,
|
||||
shipping_price_net_amount: 0,
|
||||
shipping_tax_rate: "10",
|
||||
};
|
|
@ -2,9 +2,10 @@ import { CreateTransactionModel } from "avatax/lib/models/CreateTransactionModel
|
|||
import { LineItemModel } from "avatax/lib/models/LineItemModel";
|
||||
import { TransactionModel } from "avatax/lib/models/TransactionModel";
|
||||
import { TaxBaseFragment } from "../../../../../generated/graphql";
|
||||
import { ResponseTaxPayload } from "../../types";
|
||||
|
||||
import { ChannelConfig } from "../../../channels-configuration/channels-config";
|
||||
import { taxLineResolver } from "../../tax-line-resolver";
|
||||
import { ResponseTaxPayload } from "../../types";
|
||||
import { AvataxConfig } from "./avatax-config";
|
||||
|
||||
const SHIPPING_ITEM_CODE = "Shipping";
|
||||
|
@ -88,7 +89,7 @@ const prepareResponse = (transaction: TransactionModel): ResponseTaxPayload => {
|
|||
shipping_price_gross_amount: shippingGrossAmount,
|
||||
shipping_price_net_amount: shippingNetAmount,
|
||||
// todo: add shipping tax rate
|
||||
shipping_tax_rate: "0",
|
||||
shipping_tax_rate: 0,
|
||||
lines:
|
||||
productLines?.map((line) => {
|
||||
const lineTaxCalculated = line.taxCalculated ?? 0;
|
||||
|
@ -98,7 +99,7 @@ const prepareResponse = (transaction: TransactionModel): ResponseTaxPayload => {
|
|||
total_gross_amount: lineTotalGrossAmount,
|
||||
total_net_amount: lineTotalNetAmount,
|
||||
// todo: add tax rate
|
||||
tax_rate: "0",
|
||||
tax_rate: 0,
|
||||
};
|
||||
}) ?? [],
|
||||
};
|
||||
|
|
|
@ -5,14 +5,25 @@ import {
|
|||
TaxBaseLineFragment,
|
||||
TaxDiscountFragment,
|
||||
} from "../../../../../generated/graphql";
|
||||
import { FetchTaxesLinePayload, ResponseTaxPayload } from "../../types";
|
||||
import { ChannelConfig } from "../../../channels-configuration/channels-config";
|
||||
import { taxLineResolver } from "../../tax-line-resolver";
|
||||
import { ResponseTaxPayload } from "../../types";
|
||||
|
||||
const formatCalculatedAmount = (amount: number) => {
|
||||
return Number(amount.toFixed(2));
|
||||
};
|
||||
|
||||
// * This type is related to `TaxLineItem` from TaxJar. It should be unified.
|
||||
type FetchTaxesLinePayload = {
|
||||
id: string;
|
||||
quantity: number;
|
||||
taxCode?: string | null;
|
||||
discount: number;
|
||||
chargeTaxes: boolean;
|
||||
unitAmount: number;
|
||||
totalAmount: number;
|
||||
};
|
||||
|
||||
const prepareLinesWithDiscountPayload = (
|
||||
lines: Array<TaxBaseLineFragment>,
|
||||
discounts: Array<TaxDiscountFragment>
|
||||
|
@ -66,7 +77,7 @@ const prepareResponse = (
|
|||
return {
|
||||
shipping_price_gross_amount: formatCalculatedAmount(shippingPriceGross),
|
||||
shipping_price_net_amount: formatCalculatedAmount(shippingPriceNet),
|
||||
shipping_tax_rate: String(shippingTaxRate),
|
||||
shipping_tax_rate: shippingTaxRate,
|
||||
// lines order needs to be the same as for recieved payload.
|
||||
// lines that have chargeTaxes === false will have returned default value
|
||||
lines: linesWithDiscount.map((line) => {
|
||||
|
@ -75,11 +86,11 @@ const prepareResponse = (
|
|||
? lineTax.taxable_amount + lineTax.tax_collectable
|
||||
: line.totalAmount - line.discount;
|
||||
const totalNetAmount = lineTax ? lineTax.taxable_amount : line.totalAmount - line.discount;
|
||||
const taxRate = lineTax ? String(lineTax.combined_tax_rate || 0) : "0";
|
||||
const taxRate = lineTax ? lineTax.combined_tax_rate : 0;
|
||||
return {
|
||||
total_gross_amount: formatCalculatedAmount(totalGrossAmount),
|
||||
total_net_amount: formatCalculatedAmount(totalNetAmount),
|
||||
tax_rate: taxRate,
|
||||
tax_rate: taxRate ?? 0,
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
import { SaleorSyncWebhook } from "../../lib/saleor/saleor-app-sdk";
|
||||
import { ResponseTaxPayload } from "./types";
|
||||
export class TaxSaleorSyncWebhook<TPayload = any> extends SaleorSyncWebhook<
|
||||
TPayload,
|
||||
ResponseTaxPayload
|
||||
> {}
|
|
@ -1,22 +1,3 @@
|
|||
type LineTaxResponsePayload = {
|
||||
total_gross_amount: number;
|
||||
total_net_amount: number;
|
||||
tax_rate: string;
|
||||
};
|
||||
import { SyncWebhookResponsesMap } from "@saleor/app-sdk/handlers/next";
|
||||
|
||||
export type ResponseTaxPayload = {
|
||||
shipping_price_gross_amount: number;
|
||||
shipping_price_net_amount: number;
|
||||
shipping_tax_rate: string;
|
||||
lines: LineTaxResponsePayload[];
|
||||
};
|
||||
|
||||
export type FetchTaxesLinePayload = {
|
||||
id: string;
|
||||
quantity: number;
|
||||
taxCode?: string | null;
|
||||
discount: number;
|
||||
chargeTaxes: boolean;
|
||||
unitAmount: number;
|
||||
totalAmount: number;
|
||||
};
|
||||
export type ResponseTaxPayload = SyncWebhookResponsesMap["ORDER_CALCULATE_TAXES"];
|
||||
|
|
|
@ -1,27 +1,87 @@
|
|||
import { SaleorSyncWebhook } from "@saleor/app-sdk/handlers/next";
|
||||
import { UntypedCalculateTaxesDocument } from "../../../../generated/graphql";
|
||||
import { saleorApp } from "../../../../saleor-app";
|
||||
import { createClient } from "../../../lib/graphql";
|
||||
import { createLogger } from "../../../lib/logger";
|
||||
import { withTaxesWebhook } from "../../../lib/saleor/with-taxes-webhook";
|
||||
import { calculateTaxesPayloadSchema, ExpectedWebhookPayload } from "../../../lib/saleor/schema";
|
||||
import { GetChannelsConfigurationService } from "../../../modules/channels-configuration/get-channels-configuration.service";
|
||||
import { GetProvidersConfigurationService } from "../../../modules/providers-configuration/get-providers-configuration.service";
|
||||
import { ActiveTaxProvider } from "../../../modules/taxes/active-tax-provider";
|
||||
import { TaxSaleorSyncWebhook } from "../../../modules/taxes/tax-webhook";
|
||||
|
||||
export const checkoutCalculateTaxesSyncWebhook = new TaxSaleorSyncWebhook({
|
||||
export const config = {
|
||||
api: {
|
||||
bodyParser: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const checkoutCalculateTaxesSyncWebhook = new SaleorSyncWebhook<ExpectedWebhookPayload>({
|
||||
name: "CheckoutCalculateTaxes",
|
||||
apl: saleorApp.apl,
|
||||
syncEvent: "CHECKOUT_CALCULATE_TAXES",
|
||||
subscriptionQueryAst: UntypedCalculateTaxesDocument,
|
||||
event: "CHECKOUT_CALCULATE_TAXES",
|
||||
query: UntypedCalculateTaxesDocument,
|
||||
webhookPath: "/api/webhooks/checkout-calculate-taxes",
|
||||
});
|
||||
|
||||
const handler = withTaxesWebhook(async (payload, config, res) => {
|
||||
const logger = createLogger({ fn: "Inside CHECKOUT_CALCULATE_TAXES handler" });
|
||||
logger.info("Handler called");
|
||||
const { provider, channel } = config;
|
||||
const taxProvider = new ActiveTaxProvider(provider);
|
||||
const calculatedTaxes = await taxProvider.calculate(payload.taxBase, channel);
|
||||
export default checkoutCalculateTaxesSyncWebhook.createHandler(async (req, res, ctx) => {
|
||||
const logger = createLogger({ event: ctx.event });
|
||||
const { authData, payload } = ctx;
|
||||
logger.info({ payload }, "Handler called with payload");
|
||||
|
||||
logger.info({ calculatedTaxes }, "Taxes calculated");
|
||||
return res.status(200).json(calculatedTaxes);
|
||||
const validation = calculateTaxesPayloadSchema.safeParse(payload);
|
||||
|
||||
if (!validation.success) {
|
||||
logger.error({ error: validation.error.message }, "Payload is invalid");
|
||||
logger.info("Returning no data");
|
||||
return res.send({});
|
||||
}
|
||||
|
||||
const { data } = validation;
|
||||
logger.info({ data }, "Payload is valid.");
|
||||
|
||||
try {
|
||||
const client = createClient(authData.saleorApiUrl, async () =>
|
||||
Promise.resolve({ token: authData.token })
|
||||
);
|
||||
|
||||
const providersConfig = await new GetProvidersConfigurationService({
|
||||
saleorApiUrl: authData.saleorApiUrl,
|
||||
apiClient: client,
|
||||
}).getConfiguration();
|
||||
|
||||
const channelsConfig = await new GetChannelsConfigurationService({
|
||||
saleorApiUrl: authData.saleorApiUrl,
|
||||
apiClient: client,
|
||||
}).getConfiguration();
|
||||
|
||||
logger.info({ providersConfig }, "Providers configuration returned");
|
||||
|
||||
const channelSlug = payload.taxBase.channel.slug;
|
||||
const channelConfig = channelsConfig[channelSlug];
|
||||
|
||||
if (!channelConfig) {
|
||||
logger.error(`Channel config not found for channel ${channelSlug}`);
|
||||
logger.info("Returning no data");
|
||||
return res.send({});
|
||||
}
|
||||
|
||||
const providerInstance = providersConfig.find(
|
||||
(instance) => instance.id === channelConfig.providerInstanceId
|
||||
);
|
||||
|
||||
if (!providerInstance) {
|
||||
logger.error(`Channel (${channelSlug}) providerInstanceId does not match any providers`);
|
||||
logger.info("Returning no data");
|
||||
return res.send({});
|
||||
}
|
||||
|
||||
const taxProvider = new ActiveTaxProvider(providerInstance);
|
||||
const calculatedTaxes = await taxProvider.calculate(data.taxBase, channelConfig);
|
||||
|
||||
logger.info({ calculatedTaxes }, "Taxes calculated");
|
||||
return res.send(ctx.buildResponse(calculatedTaxes));
|
||||
} catch (error) {
|
||||
logger.error({ error }, "Error while calculating taxes");
|
||||
logger.info("Returning no data");
|
||||
return res.send({});
|
||||
}
|
||||
});
|
||||
|
||||
export default checkoutCalculateTaxesSyncWebhook.createHandler(handler);
|
||||
|
|
|
@ -1,27 +1,87 @@
|
|||
import { SaleorSyncWebhook } from "@saleor/app-sdk/handlers/next";
|
||||
import { UntypedCalculateTaxesDocument } from "../../../../generated/graphql";
|
||||
import { saleorApp } from "../../../../saleor-app";
|
||||
import { createClient } from "../../../lib/graphql";
|
||||
import { createLogger } from "../../../lib/logger";
|
||||
import { withTaxesWebhook } from "../../../lib/saleor/with-taxes-webhook";
|
||||
import { calculateTaxesPayloadSchema, ExpectedWebhookPayload } from "../../../lib/saleor/schema";
|
||||
import { GetChannelsConfigurationService } from "../../../modules/channels-configuration/get-channels-configuration.service";
|
||||
import { GetProvidersConfigurationService } from "../../../modules/providers-configuration/get-providers-configuration.service";
|
||||
import { ActiveTaxProvider } from "../../../modules/taxes/active-tax-provider";
|
||||
import { TaxSaleorSyncWebhook } from "../../../modules/taxes/tax-webhook";
|
||||
|
||||
export const orderCalculateTaxesSyncWebhook = new TaxSaleorSyncWebhook({
|
||||
export const config = {
|
||||
api: {
|
||||
bodyParser: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const orderCalculateTaxesSyncWebhook = new SaleorSyncWebhook<ExpectedWebhookPayload>({
|
||||
name: "OrderCalculateTaxes",
|
||||
apl: saleorApp.apl,
|
||||
syncEvent: "ORDER_CALCULATE_TAXES",
|
||||
subscriptionQueryAst: UntypedCalculateTaxesDocument,
|
||||
event: "ORDER_CALCULATE_TAXES",
|
||||
query: UntypedCalculateTaxesDocument,
|
||||
webhookPath: "/api/webhooks/order-calculate-taxes",
|
||||
});
|
||||
|
||||
const handler = withTaxesWebhook(async (payload, config, res) => {
|
||||
const logger = createLogger({});
|
||||
logger.info("Inside ORDER_CALCULATE_TAXES handler");
|
||||
const { provider, channel } = config;
|
||||
const taxProvider = new ActiveTaxProvider(provider);
|
||||
const calculatedTaxes = await taxProvider.calculate(payload.taxBase, channel);
|
||||
export default orderCalculateTaxesSyncWebhook.createHandler(async (req, res, ctx) => {
|
||||
const logger = createLogger({ event: ctx.event });
|
||||
const { authData, payload } = ctx;
|
||||
logger.info({ payload }, "Handler called with payload");
|
||||
|
||||
logger.info({ calculatedTaxes }, "Taxes calculated");
|
||||
return res.status(200).json(calculatedTaxes);
|
||||
const validation = calculateTaxesPayloadSchema.safeParse(payload);
|
||||
|
||||
if (!validation.success) {
|
||||
logger.error({ error: validation.error.message }, "Payload is invalid");
|
||||
logger.info("Returning no data");
|
||||
return res.status(200).json({});
|
||||
}
|
||||
|
||||
const { data } = validation;
|
||||
logger.info({ data }, "Payload is valid.");
|
||||
|
||||
try {
|
||||
const client = createClient(authData.saleorApiUrl, async () =>
|
||||
Promise.resolve({ token: authData.token })
|
||||
);
|
||||
|
||||
const providersConfig = await new GetProvidersConfigurationService({
|
||||
saleorApiUrl: authData.saleorApiUrl,
|
||||
apiClient: client,
|
||||
}).getConfiguration();
|
||||
|
||||
const channelsConfig = await new GetChannelsConfigurationService({
|
||||
saleorApiUrl: authData.saleorApiUrl,
|
||||
apiClient: client,
|
||||
}).getConfiguration();
|
||||
|
||||
logger.info({ providersConfig }, "Providers configuration returned");
|
||||
|
||||
const channelSlug = payload.taxBase.channel.slug;
|
||||
const channelConfig = channelsConfig[channelSlug];
|
||||
|
||||
if (!channelConfig) {
|
||||
logger.error(`Channel config not found for channel ${channelSlug}`);
|
||||
logger.info("Returning no data");
|
||||
return res.send({});
|
||||
}
|
||||
|
||||
const providerInstance = providersConfig.find(
|
||||
(instance) => instance.id === channelConfig.providerInstanceId
|
||||
);
|
||||
|
||||
if (!providerInstance) {
|
||||
logger.error(`Channel (${channelSlug}) providerInstanceId does not match any providers`);
|
||||
logger.info("Returning no data");
|
||||
return res.send({});
|
||||
}
|
||||
|
||||
const taxProvider = new ActiveTaxProvider(providerInstance);
|
||||
const calculatedTaxes = await taxProvider.calculate(data.taxBase, channelConfig);
|
||||
|
||||
logger.info({ calculatedTaxes }, "Taxes calculated");
|
||||
return res.send(ctx.buildResponse(calculatedTaxes));
|
||||
} catch (error) {
|
||||
logger.error({ error }, "Error while calculating taxes");
|
||||
logger.info("Returning no data");
|
||||
return res.send({});
|
||||
}
|
||||
});
|
||||
|
||||
export default orderCalculateTaxesSyncWebhook.createHandler(handler);
|
||||
|
|
|
@ -659,7 +659,7 @@ importers:
|
|||
'@material-ui/core': ^4.12.4
|
||||
'@material-ui/icons': ^4.11.3
|
||||
'@material-ui/lab': 4.0.0-alpha.61
|
||||
'@saleor/app-sdk': 0.33.0
|
||||
'@saleor/app-sdk': 0.34.1
|
||||
'@saleor/apps-shared': workspace:*
|
||||
'@saleor/macaw-ui': ^0.7.2
|
||||
'@tanstack/react-query': ^4.19.1
|
||||
|
@ -704,7 +704,7 @@ importers:
|
|||
'@material-ui/core': 4.12.4_5ndqzdd6t4rivxsukjv3i3ak2q
|
||||
'@material-ui/icons': 4.11.3_x54wk6dsnsxe7g7vvfmytp77te
|
||||
'@material-ui/lab': 4.0.0-alpha.61_x54wk6dsnsxe7g7vvfmytp77te
|
||||
'@saleor/app-sdk': 0.33.0_nvzgbose6yf6w7ijjprgspqefi
|
||||
'@saleor/app-sdk': 0.34.1_nvzgbose6yf6w7ijjprgspqefi
|
||||
'@saleor/apps-shared': link:../../packages/shared
|
||||
'@saleor/macaw-ui': 0.7.2_pmlnlm755hlzzzocw2qhf3a34e
|
||||
'@tanstack/react-query': 4.24.4_biqbaboplfbrettd7655fr4n2y
|
||||
|
@ -4335,8 +4335,8 @@ packages:
|
|||
- supports-color
|
||||
dev: false
|
||||
|
||||
/@saleor/app-sdk/0.33.0_nvzgbose6yf6w7ijjprgspqefi:
|
||||
resolution: {integrity: sha512-JEeUTBIa2aRuIBEeV1U/C76Oy4hfdMnXo16XB+PH/wQUPSBIsKkfpAB82JmAQiLzHHvTh5V9QZynPqt8EZbCFw==}
|
||||
/@saleor/app-sdk/0.34.1_nvzgbose6yf6w7ijjprgspqefi:
|
||||
resolution: {integrity: sha512-z5oheqsJziYgsCHg2Q3x1EaiIBaWh7Y1DcAg/wGwxaI/1CLS2i4jKoTjTTvO0MzGNdZQdQRkQIQ8LQBwqQRlzQ==}
|
||||
peerDependencies:
|
||||
next: '>=12'
|
||||
react: '>=17'
|
||||
|
|
Loading…
Reference in a new issue