refactor: ♻️ use app-sdk syncwebhook
This commit is contained in:
parent
0d75d3a311
commit
9138d7debd
13 changed files with 186 additions and 355 deletions
|
@ -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,24 @@ 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));
|
||||
};
|
||||
|
||||
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 +76,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 +85,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,95 @@
|
|||
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);
|
||||
if (!authData) {
|
||||
logger.error("Auth data not found");
|
||||
logger.info("Returning no data");
|
||||
return res.send({});
|
||||
}
|
||||
|
||||
logger.info("Parsing payload...");
|
||||
|
||||
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,95 @@
|
|||
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);
|
||||
if (!authData) {
|
||||
logger.error("Auth data not found");
|
||||
logger.info("Returning no data");
|
||||
return res.status(200).json({});
|
||||
}
|
||||
|
||||
logger.info("Parsing payload...");
|
||||
|
||||
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);
|
||||
|
|
Loading…
Reference in a new issue