diff --git a/.changeset/real-tigers-fry.md b/.changeset/real-tigers-fry.md new file mode 100644 index 0000000..9f0db2c --- /dev/null +++ b/.changeset/real-tigers-fry.md @@ -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. diff --git a/apps/taxes/package.json b/apps/taxes/package.json index bad6d8a..8385ada 100644 --- a/apps/taxes/package.json +++ b/apps/taxes/package.json @@ -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", diff --git a/apps/taxes/src/lib/saleor/saleor-app-sdk/index.ts b/apps/taxes/src/lib/saleor/saleor-app-sdk/index.ts deleted file mode 100644 index 4f9cfb3..0000000 --- a/apps/taxes/src/lib/saleor/saleor-app-sdk/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./saleor-app-sdk-copy"; -export * from "./saleor-sync-webhook"; -export * from "./saleor-webhook"; diff --git a/apps/taxes/src/lib/saleor/saleor-app-sdk/saleor-app-sdk-copy.ts b/apps/taxes/src/lib/saleor/saleor-app-sdk/saleor-app-sdk-copy.ts deleted file mode 100644 index 9febf2b..0000000 --- a/apps/taxes/src/lib/saleor/saleor-app-sdk/saleor-app-sdk-copy.ts +++ /dev/null @@ -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 = { - 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 diff --git a/apps/taxes/src/lib/saleor/saleor-app-sdk/saleor-sync-webhook.ts b/apps/taxes/src/lib/saleor/saleor-app-sdk/saleor-sync-webhook.ts deleted file mode 100644 index 40c6e69..0000000 --- a/apps/taxes/src/lib/saleor/saleor-app-sdk/saleor-sync-webhook.ts +++ /dev/null @@ -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 - implements SaleorWebhook -{ - 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): NextApiHandler { - 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 = { - authData, - baseUrl, - event: this.event, - payload: req.body, - }; - return handlerFn(req, res, context); - }; - } -} diff --git a/apps/taxes/src/lib/saleor/saleor-app-sdk/saleor-webhook.ts b/apps/taxes/src/lib/saleor/saleor-app-sdk/saleor-webhook.ts deleted file mode 100644 index 08e0510..0000000 --- a/apps/taxes/src/lib/saleor/saleor-app-sdk/saleor-webhook.ts +++ /dev/null @@ -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 { - 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): NextApiHandler; -} diff --git a/apps/taxes/src/lib/saleor/with-taxes-webhook.ts b/apps/taxes/src/lib/saleor/with-taxes-webhook.ts deleted file mode 100644 index 08ff1aa..0000000 --- a/apps/taxes/src/lib/saleor/with-taxes-webhook.ts +++ /dev/null @@ -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 - ) => Promise - ) => - async (req: NextApiRequest, res: NextApiResponse, context: WebhookContext) => { - 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); - } - }; diff --git a/apps/taxes/src/modules/taxes/defaults.ts b/apps/taxes/src/modules/taxes/defaults.ts deleted file mode 100644 index 652cbdc..0000000 --- a/apps/taxes/src/modules/taxes/defaults.ts +++ /dev/null @@ -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", -}; diff --git a/apps/taxes/src/modules/taxes/mocks.ts b/apps/taxes/src/modules/taxes/mocks.ts deleted file mode 100644 index 30b3642..0000000 --- a/apps/taxes/src/modules/taxes/mocks.ts +++ /dev/null @@ -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", -}; diff --git a/apps/taxes/src/modules/taxes/providers/avatax/avatax-calculate.ts b/apps/taxes/src/modules/taxes/providers/avatax/avatax-calculate.ts index 4405a87..7b520fe 100644 --- a/apps/taxes/src/modules/taxes/providers/avatax/avatax-calculate.ts +++ b/apps/taxes/src/modules/taxes/providers/avatax/avatax-calculate.ts @@ -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, }; }) ?? [], }; diff --git a/apps/taxes/src/modules/taxes/providers/taxjar/taxjar-calculate.ts b/apps/taxes/src/modules/taxes/providers/taxjar/taxjar-calculate.ts index 4b2f394..9be401a 100644 --- a/apps/taxes/src/modules/taxes/providers/taxjar/taxjar-calculate.ts +++ b/apps/taxes/src/modules/taxes/providers/taxjar/taxjar-calculate.ts @@ -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, discounts: Array @@ -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, }; }), }; diff --git a/apps/taxes/src/modules/taxes/tax-webhook.ts b/apps/taxes/src/modules/taxes/tax-webhook.ts deleted file mode 100644 index ff72843..0000000 --- a/apps/taxes/src/modules/taxes/tax-webhook.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { SaleorSyncWebhook } from "../../lib/saleor/saleor-app-sdk"; -import { ResponseTaxPayload } from "./types"; -export class TaxSaleorSyncWebhook extends SaleorSyncWebhook< - TPayload, - ResponseTaxPayload -> {} diff --git a/apps/taxes/src/modules/taxes/types.ts b/apps/taxes/src/modules/taxes/types.ts index f5b1ff4..fea6042 100644 --- a/apps/taxes/src/modules/taxes/types.ts +++ b/apps/taxes/src/modules/taxes/types.ts @@ -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"]; diff --git a/apps/taxes/src/pages/api/webhooks/checkout-calculate-taxes.ts b/apps/taxes/src/pages/api/webhooks/checkout-calculate-taxes.ts index 5c28df4..db9ddc7 100644 --- a/apps/taxes/src/pages/api/webhooks/checkout-calculate-taxes.ts +++ b/apps/taxes/src/pages/api/webhooks/checkout-calculate-taxes.ts @@ -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({ 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); diff --git a/apps/taxes/src/pages/api/webhooks/order-calculate-taxes.ts b/apps/taxes/src/pages/api/webhooks/order-calculate-taxes.ts index e145e00..c67d57f 100644 --- a/apps/taxes/src/pages/api/webhooks/order-calculate-taxes.ts +++ b/apps/taxes/src/pages/api/webhooks/order-calculate-taxes.ts @@ -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({ 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); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e12bde8..9938970 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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'