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 { LineItemModel } from "avatax/lib/models/LineItemModel";
|
||||||
import { TransactionModel } from "avatax/lib/models/TransactionModel";
|
import { TransactionModel } from "avatax/lib/models/TransactionModel";
|
||||||
import { TaxBaseFragment } from "../../../../../generated/graphql";
|
import { TaxBaseFragment } from "../../../../../generated/graphql";
|
||||||
import { ResponseTaxPayload } from "../../types";
|
|
||||||
import { ChannelConfig } from "../../../channels-configuration/channels-config";
|
import { ChannelConfig } from "../../../channels-configuration/channels-config";
|
||||||
import { taxLineResolver } from "../../tax-line-resolver";
|
import { taxLineResolver } from "../../tax-line-resolver";
|
||||||
|
import { ResponseTaxPayload } from "../../types";
|
||||||
import { AvataxConfig } from "./avatax-config";
|
import { AvataxConfig } from "./avatax-config";
|
||||||
|
|
||||||
const SHIPPING_ITEM_CODE = "Shipping";
|
const SHIPPING_ITEM_CODE = "Shipping";
|
||||||
|
@ -88,7 +89,7 @@ const prepareResponse = (transaction: TransactionModel): ResponseTaxPayload => {
|
||||||
shipping_price_gross_amount: shippingGrossAmount,
|
shipping_price_gross_amount: shippingGrossAmount,
|
||||||
shipping_price_net_amount: shippingNetAmount,
|
shipping_price_net_amount: shippingNetAmount,
|
||||||
// todo: add shipping tax rate
|
// todo: add shipping tax rate
|
||||||
shipping_tax_rate: "0",
|
shipping_tax_rate: 0,
|
||||||
lines:
|
lines:
|
||||||
productLines?.map((line) => {
|
productLines?.map((line) => {
|
||||||
const lineTaxCalculated = line.taxCalculated ?? 0;
|
const lineTaxCalculated = line.taxCalculated ?? 0;
|
||||||
|
@ -98,7 +99,7 @@ const prepareResponse = (transaction: TransactionModel): ResponseTaxPayload => {
|
||||||
total_gross_amount: lineTotalGrossAmount,
|
total_gross_amount: lineTotalGrossAmount,
|
||||||
total_net_amount: lineTotalNetAmount,
|
total_net_amount: lineTotalNetAmount,
|
||||||
// todo: add tax rate
|
// todo: add tax rate
|
||||||
tax_rate: "0",
|
tax_rate: 0,
|
||||||
};
|
};
|
||||||
}) ?? [],
|
}) ?? [],
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,14 +5,24 @@ import {
|
||||||
TaxBaseLineFragment,
|
TaxBaseLineFragment,
|
||||||
TaxDiscountFragment,
|
TaxDiscountFragment,
|
||||||
} from "../../../../../generated/graphql";
|
} from "../../../../../generated/graphql";
|
||||||
import { FetchTaxesLinePayload, ResponseTaxPayload } from "../../types";
|
|
||||||
import { ChannelConfig } from "../../../channels-configuration/channels-config";
|
import { ChannelConfig } from "../../../channels-configuration/channels-config";
|
||||||
import { taxLineResolver } from "../../tax-line-resolver";
|
import { taxLineResolver } from "../../tax-line-resolver";
|
||||||
|
import { ResponseTaxPayload } from "../../types";
|
||||||
|
|
||||||
const formatCalculatedAmount = (amount: number) => {
|
const formatCalculatedAmount = (amount: number) => {
|
||||||
return Number(amount.toFixed(2));
|
return Number(amount.toFixed(2));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type FetchTaxesLinePayload = {
|
||||||
|
id: string;
|
||||||
|
quantity: number;
|
||||||
|
taxCode?: string | null;
|
||||||
|
discount: number;
|
||||||
|
chargeTaxes: boolean;
|
||||||
|
unitAmount: number;
|
||||||
|
totalAmount: number;
|
||||||
|
};
|
||||||
|
|
||||||
const prepareLinesWithDiscountPayload = (
|
const prepareLinesWithDiscountPayload = (
|
||||||
lines: Array<TaxBaseLineFragment>,
|
lines: Array<TaxBaseLineFragment>,
|
||||||
discounts: Array<TaxDiscountFragment>
|
discounts: Array<TaxDiscountFragment>
|
||||||
|
@ -66,7 +76,7 @@ const prepareResponse = (
|
||||||
return {
|
return {
|
||||||
shipping_price_gross_amount: formatCalculatedAmount(shippingPriceGross),
|
shipping_price_gross_amount: formatCalculatedAmount(shippingPriceGross),
|
||||||
shipping_price_net_amount: formatCalculatedAmount(shippingPriceNet),
|
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 order needs to be the same as for recieved payload.
|
||||||
// lines that have chargeTaxes === false will have returned default value
|
// lines that have chargeTaxes === false will have returned default value
|
||||||
lines: linesWithDiscount.map((line) => {
|
lines: linesWithDiscount.map((line) => {
|
||||||
|
@ -75,11 +85,11 @@ const prepareResponse = (
|
||||||
? lineTax.taxable_amount + lineTax.tax_collectable
|
? lineTax.taxable_amount + lineTax.tax_collectable
|
||||||
: line.totalAmount - line.discount;
|
: line.totalAmount - line.discount;
|
||||||
const totalNetAmount = lineTax ? lineTax.taxable_amount : 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 {
|
return {
|
||||||
total_gross_amount: formatCalculatedAmount(totalGrossAmount),
|
total_gross_amount: formatCalculatedAmount(totalGrossAmount),
|
||||||
total_net_amount: formatCalculatedAmount(totalNetAmount),
|
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 = {
|
import { SyncWebhookResponsesMap } from "@saleor/app-sdk/handlers/next";
|
||||||
total_gross_amount: number;
|
|
||||||
total_net_amount: number;
|
|
||||||
tax_rate: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ResponseTaxPayload = {
|
export type ResponseTaxPayload = SyncWebhookResponsesMap["ORDER_CALCULATE_TAXES"];
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,27 +1,95 @@
|
||||||
|
import { SaleorSyncWebhook } from "@saleor/app-sdk/handlers/next";
|
||||||
import { UntypedCalculateTaxesDocument } from "../../../../generated/graphql";
|
import { UntypedCalculateTaxesDocument } from "../../../../generated/graphql";
|
||||||
import { saleorApp } from "../../../../saleor-app";
|
import { saleorApp } from "../../../../saleor-app";
|
||||||
|
import { createClient } from "../../../lib/graphql";
|
||||||
import { createLogger } from "../../../lib/logger";
|
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 { 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",
|
name: "CheckoutCalculateTaxes",
|
||||||
apl: saleorApp.apl,
|
apl: saleorApp.apl,
|
||||||
syncEvent: "CHECKOUT_CALCULATE_TAXES",
|
event: "CHECKOUT_CALCULATE_TAXES",
|
||||||
subscriptionQueryAst: UntypedCalculateTaxesDocument,
|
query: UntypedCalculateTaxesDocument,
|
||||||
webhookPath: "/api/webhooks/checkout-calculate-taxes",
|
webhookPath: "/api/webhooks/checkout-calculate-taxes",
|
||||||
});
|
});
|
||||||
|
|
||||||
const handler = withTaxesWebhook(async (payload, config, res) => {
|
export default checkoutCalculateTaxesSyncWebhook.createHandler(async (req, res, ctx) => {
|
||||||
const logger = createLogger({ fn: "Inside CHECKOUT_CALCULATE_TAXES handler" });
|
const logger = createLogger({ event: ctx.event });
|
||||||
logger.info("Handler called");
|
const { authData, payload } = ctx;
|
||||||
const { provider, channel } = config;
|
logger.info({ payload }, "Handler called with payload");
|
||||||
const taxProvider = new ActiveTaxProvider(provider);
|
|
||||||
const calculatedTaxes = await taxProvider.calculate(payload.taxBase, channel);
|
|
||||||
|
|
||||||
logger.info({ calculatedTaxes }, "Taxes calculated");
|
if (!authData) {
|
||||||
return res.status(200).json(calculatedTaxes);
|
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 { UntypedCalculateTaxesDocument } from "../../../../generated/graphql";
|
||||||
import { saleorApp } from "../../../../saleor-app";
|
import { saleorApp } from "../../../../saleor-app";
|
||||||
|
import { createClient } from "../../../lib/graphql";
|
||||||
import { createLogger } from "../../../lib/logger";
|
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 { 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",
|
name: "OrderCalculateTaxes",
|
||||||
apl: saleorApp.apl,
|
apl: saleorApp.apl,
|
||||||
syncEvent: "ORDER_CALCULATE_TAXES",
|
event: "ORDER_CALCULATE_TAXES",
|
||||||
subscriptionQueryAst: UntypedCalculateTaxesDocument,
|
query: UntypedCalculateTaxesDocument,
|
||||||
webhookPath: "/api/webhooks/order-calculate-taxes",
|
webhookPath: "/api/webhooks/order-calculate-taxes",
|
||||||
});
|
});
|
||||||
|
|
||||||
const handler = withTaxesWebhook(async (payload, config, res) => {
|
export default orderCalculateTaxesSyncWebhook.createHandler(async (req, res, ctx) => {
|
||||||
const logger = createLogger({});
|
const logger = createLogger({ event: ctx.event });
|
||||||
logger.info("Inside ORDER_CALCULATE_TAXES handler");
|
const { authData, payload } = ctx;
|
||||||
const { provider, channel } = config;
|
logger.info({ payload }, "Handler called with payload");
|
||||||
const taxProvider = new ActiveTaxProvider(provider);
|
|
||||||
const calculatedTaxes = await taxProvider.calculate(payload.taxBase, channel);
|
|
||||||
|
|
||||||
logger.info({ calculatedTaxes }, "Taxes calculated");
|
if (!authData) {
|
||||||
return res.status(200).json(calculatedTaxes);
|
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