diff --git a/.changeset/moody-taxis-peel.md b/.changeset/moody-taxis-peel.md new file mode 100644 index 000000000..2021de6d2 --- /dev/null +++ b/.changeset/moody-taxis-peel.md @@ -0,0 +1,5 @@ +--- +"app-avatax": patch +--- + +Refactored so called "webhook service" class. Now each webhook creates it's own dependencies. It's a part of larger refactor that aims to simplify app's architecture. No functional change is expected. diff --git a/apps/avatax/src/modules/avatax/avatax-webhook.service.ts b/apps/avatax/src/modules/avatax/avatax-webhook.service.ts deleted file mode 100644 index 553e57128..000000000 --- a/apps/avatax/src/modules/avatax/avatax-webhook.service.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { AuthData } from "@saleor/app-sdk/APL"; - -import { AvataxCalculateTaxesPayloadService } from "@/modules/avatax/calculate-taxes/avatax-calculate-taxes-payload.service"; -import { AvataxCalculateTaxesPayloadTransformer } from "@/modules/avatax/calculate-taxes/avatax-calculate-taxes-payload-transformer"; -import { AvataxTaxCodeMatchesService } from "@/modules/avatax/tax-code/avatax-tax-code-matches.service"; - -import { SaleorOrderConfirmedEvent } from "../saleor"; -import { CancelOrderPayload } from "../taxes/tax-provider-webhook"; -import { CalculateTaxesPayload } from "../webhooks/payloads/calculate-taxes-payload"; -import { AvataxConfig } from "./avatax-connection-schema"; -import { AvataxCalculateTaxesAdapter } from "./calculate-taxes/avatax-calculate-taxes-adapter"; -import { - AutomaticallyDistributedProductLinesDiscountsStrategy, - PriceReductionDiscountsStrategy, -} from "./discounts"; -import { AvataxOrderCancelledAdapter } from "./order-cancelled/avatax-order-cancelled-adapter"; -import { AvataxOrderConfirmedAdapter } from "./order-confirmed/avatax-order-confirmed-adapter"; - -export class AvataxWebhookService { - constructor( - private calculateTaxesAdapter: AvataxCalculateTaxesAdapter, - private calculateTaxesPayloadTransformer: AvataxCalculateTaxesPayloadTransformer, - private avaTaxOrderCancelledAdapter: AvataxOrderCancelledAdapter, - private avataxOrderConfirmedAdapter: AvataxOrderConfirmedAdapter, - ) {} - - async calculateTaxes( - payload: CalculateTaxesPayload, - avataxConfig: AvataxConfig, - authData: AuthData, - discountStrategy: AutomaticallyDistributedProductLinesDiscountsStrategy, - ) { - const payloadService = new AvataxCalculateTaxesPayloadService( - AvataxTaxCodeMatchesService.createFromAuthData(authData), - this.calculateTaxesPayloadTransformer, - ); - - const avataxModel = await payloadService.getPayload(payload, avataxConfig, discountStrategy); - - const response = await this.calculateTaxesAdapter.send(avataxModel); - - return response; - } - - async confirmOrder( - confirmedOrderEvent: SaleorOrderConfirmedEvent, - avataxConfig: AvataxConfig, - authData: AuthData, - discountStrategy: PriceReductionDiscountsStrategy, - ) { - const response = await this.avataxOrderConfirmedAdapter.send( - { confirmedOrderEvent }, - avataxConfig, - authData, - discountStrategy, - ); - - return response; - } - - async cancelOrder(payload: CancelOrderPayload, avataxConfig: AvataxConfig) { - await this.avaTaxOrderCancelledAdapter.send(payload, avataxConfig); - } -} diff --git a/apps/avatax/src/modules/avatax/order-cancelled/avatax-order-cancelled-adapter-factory.ts b/apps/avatax/src/modules/avatax/order-cancelled/avatax-order-cancelled-adapter-factory.ts new file mode 100644 index 000000000..3e5d3d6aa --- /dev/null +++ b/apps/avatax/src/modules/avatax/order-cancelled/avatax-order-cancelled-adapter-factory.ts @@ -0,0 +1,14 @@ +import { AvataxClient } from "@/modules/avatax/avatax-client"; +import { AvataxConfig } from "@/modules/avatax/avatax-connection-schema"; +import { AvataxSdkClientFactory } from "@/modules/avatax/avatax-sdk-client-factory"; +import { AvataxOrderCancelledAdapter } from "@/modules/avatax/order-cancelled/avatax-order-cancelled-adapter"; +import { AvataxOrderCancelledPayloadTransformer } from "@/modules/avatax/order-cancelled/avatax-order-cancelled-payload-transformer"; + +export const createAvaTaxOrderCancelledAdapterFromConfig = (avataxConfig: AvataxConfig) => { + const avaTaxSdk = new AvataxSdkClientFactory().createClient(avataxConfig); + const avaTaxClient = new AvataxClient(avaTaxSdk); + + const avataxOrderCancelledPayloadTransformer = new AvataxOrderCancelledPayloadTransformer(); + + return new AvataxOrderCancelledAdapter(avaTaxClient, avataxOrderCancelledPayloadTransformer); +}; diff --git a/apps/avatax/src/modules/avatax/order-confirmed/avatax-order-confirmed-adapter-factory.ts b/apps/avatax/src/modules/avatax/order-confirmed/avatax-order-confirmed-adapter-factory.ts new file mode 100644 index 000000000..a89cedb7f --- /dev/null +++ b/apps/avatax/src/modules/avatax/order-confirmed/avatax-order-confirmed-adapter-factory.ts @@ -0,0 +1,43 @@ +import { AvataxCalculationDateResolver } from "@/modules/avatax/avatax-calculation-date-resolver"; +import { AvataxClient } from "@/modules/avatax/avatax-client"; +import { AvataxConfig } from "@/modules/avatax/avatax-connection-schema"; +import { AvataxDocumentCodeResolver } from "@/modules/avatax/avatax-document-code-resolver"; +import { AvataxEntityTypeMatcher } from "@/modules/avatax/avatax-entity-type-matcher"; +import { AvataxSdkClientFactory } from "@/modules/avatax/avatax-sdk-client-factory"; +import { AvataxOrderConfirmedAdapter } from "@/modules/avatax/order-confirmed/avatax-order-confirmed-adapter"; +import { AvataxOrderConfirmedPayloadService } from "@/modules/avatax/order-confirmed/avatax-order-confirmed-payload.service"; +import { AvataxOrderConfirmedPayloadTransformer } from "@/modules/avatax/order-confirmed/avatax-order-confirmed-payload-transformer"; +import { AvataxOrderConfirmedResponseTransformer } from "@/modules/avatax/order-confirmed/avatax-order-confirmed-response-transformer"; +import { SaleorOrderToAvataxLinesTransformer } from "@/modules/avatax/order-confirmed/saleor-order-to-avatax-lines-transformer"; + +/** + * Wrap all deps to create this service from minimum possible value (avatax config) + * + * Once we refactor these services to not require such configs, these should be created top level on each webhook + */ +export const createAvaTaxOrderConfirmedAdapterFromAvaTaxConfig = (config: AvataxConfig) => { + const avaTaxSdk = new AvataxSdkClientFactory().createClient(config); + const avaTaxClient = new AvataxClient(avaTaxSdk); + const entityTypeMatcher = new AvataxEntityTypeMatcher(avaTaxClient); + + const orderToAvataxLinesTransformer = new SaleorOrderToAvataxLinesTransformer(); + const calculationDateResolver = new AvataxCalculationDateResolver(); + const documentCodeResolver = new AvataxDocumentCodeResolver(); + const avataxOrderConfirmedResponseTransformer = new AvataxOrderConfirmedResponseTransformer(); + const orderConfirmedPayloadTransformer = new AvataxOrderConfirmedPayloadTransformer( + orderToAvataxLinesTransformer, + entityTypeMatcher, + calculationDateResolver, + documentCodeResolver, + ); + + const avataxOrderConfirmedPayloadService = new AvataxOrderConfirmedPayloadService( + orderConfirmedPayloadTransformer, + ); + + return new AvataxOrderConfirmedAdapter( + avaTaxClient, + avataxOrderConfirmedResponseTransformer, + avataxOrderConfirmedPayloadService, + ); +}; diff --git a/apps/avatax/src/modules/calculate-taxes/use-case/calculate-taxes.use-case.test.ts b/apps/avatax/src/modules/calculate-taxes/use-case/calculate-taxes.use-case.test.ts index eaa433fc5..66c0e22cc 100644 --- a/apps/avatax/src/modules/calculate-taxes/use-case/calculate-taxes.use-case.test.ts +++ b/apps/avatax/src/modules/calculate-taxes/use-case/calculate-taxes.use-case.test.ts @@ -2,6 +2,9 @@ import { AuthData } from "@saleor/app-sdk/APL"; import { err, ok, Result } from "neverthrow"; import { beforeEach, describe, expect, it, vi } from "vitest"; +import { AvataxCalculateTaxesPayloadLinesTransformer } from "@/modules/avatax/calculate-taxes/avatax-calculate-taxes-payload-lines-transformer"; +import { AvataxCalculateTaxesResponseTransformer } from "@/modules/avatax/calculate-taxes/avatax-calculate-taxes-response-transformer"; +import { AvataxCalculateTaxesTaxCodeMatcher } from "@/modules/avatax/calculate-taxes/avatax-calculate-taxes-tax-code-matcher"; import { SHIPPING_ITEM_CODE } from "@/modules/avatax/calculate-taxes/avatax-shipping-line"; import { ILogWriter, LogWriterContext, NoopLogWriter } from "@/modules/client-logs/log-writer"; @@ -10,7 +13,6 @@ import { AppConfig } from "../../../lib/app-config"; import { AppConfigExtractor, IAppConfigExtractor } from "../../../lib/app-config-extractor"; import { AvataxClient } from "../../avatax/avatax-client"; import { AvataxSdkClientFactory } from "../../avatax/avatax-sdk-client-factory"; -import { AvataxWebhookServiceFactory } from "../../taxes/avatax-webhook-service-factory"; import { CalculateTaxesPayload } from "../../webhooks/payloads/calculate-taxes-payload"; import { CalculateTaxesUseCase } from "./calculate-taxes.use-case"; @@ -177,6 +179,10 @@ describe("CalculateTaxesUseCase", () => { return logWriter; }, }, + calculateTaxesResponseTransformer: new AvataxCalculateTaxesResponseTransformer(), + payloadLinesTransformer: new AvataxCalculateTaxesPayloadLinesTransformer( + new AvataxCalculateTaxesTaxCodeMatcher(), + ), }); }); @@ -209,7 +215,7 @@ describe("CalculateTaxesUseCase", () => { const error = result._unsafeUnwrapErr(); expect(error).toBeInstanceOf(CalculateTaxesUseCase.ConfigBrokenError); - expect(error.errors![0]).toBeInstanceOf(AvataxWebhookServiceFactory.BrokenConfigurationError); + expect(error.errors![0]).toBeInstanceOf(BaseError); }); it("Returns XXX error if taxes calculation fails", async () => { diff --git a/apps/avatax/src/modules/calculate-taxes/use-case/calculate-taxes.use-case.ts b/apps/avatax/src/modules/calculate-taxes/use-case/calculate-taxes.use-case.ts index a2215643d..de7bb718b 100644 --- a/apps/avatax/src/modules/calculate-taxes/use-case/calculate-taxes.use-case.ts +++ b/apps/avatax/src/modules/calculate-taxes/use-case/calculate-taxes.use-case.ts @@ -1,9 +1,17 @@ import { AuthData } from "@saleor/app-sdk/APL"; -import * as Sentry from "@sentry/nextjs"; import { captureException } from "@sentry/nextjs"; import { err, fromPromise, Result } from "neverthrow"; +import { AvataxClient } from "@/modules/avatax/avatax-client"; +import { AvataxConfig } from "@/modules/avatax/avatax-connection-schema"; +import { AvataxEntityTypeMatcher } from "@/modules/avatax/avatax-entity-type-matcher"; +import { AvataxSdkClientFactory } from "@/modules/avatax/avatax-sdk-client-factory"; +import { AvataxCalculateTaxesPayloadService } from "@/modules/avatax/calculate-taxes/avatax-calculate-taxes-payload.service"; +import { AvataxCalculateTaxesPayloadLinesTransformer } from "@/modules/avatax/calculate-taxes/avatax-calculate-taxes-payload-lines-transformer"; +import { AvataxCalculateTaxesPayloadTransformer } from "@/modules/avatax/calculate-taxes/avatax-calculate-taxes-payload-transformer"; +import { AvataxCalculateTaxesResponseTransformer } from "@/modules/avatax/calculate-taxes/avatax-calculate-taxes-response-transformer"; import { AutomaticallyDistributedProductLinesDiscountsStrategy } from "@/modules/avatax/discounts"; +import { AvataxTaxCodeMatchesService } from "@/modules/avatax/tax-code/avatax-tax-code-matches.service"; import { ClientLogStoreRequest } from "@/modules/client-logs/client-log"; import { ILogWriterFactory } from "@/modules/client-logs/log-writer-factory"; @@ -12,7 +20,10 @@ import { BaseError } from "../../../error"; import { AppConfigExtractor, IAppConfigExtractor } from "../../../lib/app-config-extractor"; import { AppConfigurationLogger } from "../../../lib/app-configuration-logger"; import { createLogger } from "../../../logger"; -import { AvataxCalculateTaxesResponse } from "../../avatax/calculate-taxes/avatax-calculate-taxes-adapter"; +import { + AvataxCalculateTaxesAdapter, + AvataxCalculateTaxesResponse, +} from "../../avatax/calculate-taxes/avatax-calculate-taxes-adapter"; import { TaxIncompletePayloadErrors } from "../../taxes/tax-error"; import { CalculateTaxesPayload } from "../../webhooks/payloads/calculate-taxes-payload"; import { verifyCalculateTaxesPayload } from "../../webhooks/validate-webhook-payload"; @@ -36,6 +47,8 @@ export class CalculateTaxesUseCase { private deps: { configExtractor: IAppConfigExtractor; logWriterFactory: ILogWriterFactory; + payloadLinesTransformer: AvataxCalculateTaxesPayloadLinesTransformer; + calculateTaxesResponseTransformer: AvataxCalculateTaxesResponseTransformer; }, ) {} @@ -83,6 +96,39 @@ export class CalculateTaxesUseCase { }); } + private async callAvaTax( + payload: CalculateTaxesPayload, + avataxConfig: AvataxConfig, + discountStrategy: AutomaticallyDistributedProductLinesDiscountsStrategy, + authData: AuthData, + ) { + /** + * Create local dependencies. They more-or-less need runtime values, like AuthData. + * This is part of the refactor. Later we should refactor these and inject them into use-case + */ + const avaTaxSdk = new AvataxSdkClientFactory().createClient(avataxConfig); + const avaTaxClient = new AvataxClient(avaTaxSdk); + + const calculateTaxesAdapter = new AvataxCalculateTaxesAdapter( + avaTaxClient, + this.deps.calculateTaxesResponseTransformer, + ); + + const payloadService = new AvataxCalculateTaxesPayloadService( + AvataxTaxCodeMatchesService.createFromAuthData(authData), + new AvataxCalculateTaxesPayloadTransformer( + this.deps.payloadLinesTransformer, + new AvataxEntityTypeMatcher(avaTaxClient), + ), + ); + + const avataxModel = await payloadService.getPayload(payload, avataxConfig, discountStrategy); + + const response = await calculateTaxesAdapter.send(avataxModel); + + return response; + } + async calculateTaxes( payload: CalculateTaxesPayload, authData: AuthData, @@ -127,60 +173,8 @@ export class CalculateTaxesUseCase { ); } - const AvataxWebhookServiceFactory = await import( - "../../../modules/taxes/avatax-webhook-service-factory" - ).then((m) => m.AvataxWebhookServiceFactory); - - const webhookServiceResult = AvataxWebhookServiceFactory.createFromConfig( - config.value, - channelSlug, - ).mapErr((innerError) => { - this.logger.warn( - `Error in taxes calculation occurred: ${innerError.name} ${innerError.message}`, - { - error: innerError, - }, - ); - - switch (innerError["constructor"]) { - case AvataxWebhookServiceFactory.BrokenConfigurationError: { - return err( - new CalculateTaxesUseCase.ConfigBrokenError( - "Failed to create instance of AvaTax connection due to invalid config", - { - errors: [innerError], - }, - ), - ); - } - default: { - Sentry.captureException(innerError); - this.logger.fatal("Unhandled error", { error: innerError }); - - return err( - new CalculateTaxesUseCase.UnhandledError("Unhandled error", { errors: [innerError] }), - ); - } - } - }); - - if (webhookServiceResult.isErr()) { - ClientLogStoreRequest.create({ - level: "error", - message: "Failed to calculate taxes. Invalid config", - checkoutOrOrderId: payload.taxBase.sourceObject.id, - channelId: payload.taxBase.channel.slug, - checkoutOrOrder: "checkout", - }) - .mapErr(captureException) - .map(logWriter.writeLog); - - return webhookServiceResult.error; - } - this.logger.info("Found active connection service. Calculating taxes..."); - const { taxProvider } = webhookServiceResult.value; const providerConfig = config.value.getConfigForChannelSlug(channelSlug); if (providerConfig.isErr()) { @@ -205,11 +199,11 @@ export class CalculateTaxesUseCase { } return fromPromise( - taxProvider.calculateTaxes( + this.callAvaTax( payload, providerConfig.value.avataxConfig.config, - authData, this.discountsStrategy, + authData, ), (err) => { ClientLogStoreRequest.create({ diff --git a/apps/avatax/src/modules/taxes/avatax-webhook-service-factory.test.ts b/apps/avatax/src/modules/taxes/avatax-webhook-service-factory.test.ts deleted file mode 100644 index 901fbee0b..000000000 --- a/apps/avatax/src/modules/taxes/avatax-webhook-service-factory.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { AppConfig } from "../../lib/app-config"; -import { ChannelsConfig } from "../channel-configuration/channel-config"; -import { ProviderConnections } from "../provider-connections/provider-connections"; -import { AvataxWebhookServiceFactory } from "./avatax-webhook-service-factory"; - -const mockedProviders: ProviderConnections = [ - { - provider: "avatax", - id: "1", - config: { - isDocumentRecordingEnabled: true, - companyCode: "DEFAULT", - isAutocommit: false, - isSandbox: true, - name: "avatax-1", - shippingTaxCode: "FR000000", - credentials: { - password: "avatax-password", - username: "avatax-username", - }, - address: { - city: "New York", - country: "US", - state: "NY", - street: "123 Main St", - zip: "10001", - }, - }, - }, -]; - -const mockedChannelsWithInvalidProviderConnectionId: ChannelsConfig = [ - { - id: "1", - config: { - providerConnectionId: "3", - slug: "default-channel", - }, - }, -]; - -const mockedValidChannels: ChannelsConfig = [ - { - id: "1", - config: { - providerConnectionId: "1", - slug: "default-channel", - }, - }, -]; - -describe("AvataxWebhookServiceFactory", () => { - it("throws BrokenConfigurationError error when no providerConnectionId was found", () => { - const result = AvataxWebhookServiceFactory.createFromConfig( - AppConfig.createFromParsedConfig({ - channels: mockedChannelsWithInvalidProviderConnectionId, - providerConnections: mockedProviders, - }), - "default-channel", - ); - - expect(result._unsafeUnwrapErr()).toBeInstanceOf( - AvataxWebhookServiceFactory.BrokenConfigurationError, - ); - }); - - it("returns provider when data is correct", () => { - const result = AvataxWebhookServiceFactory.createFromConfig( - AppConfig.createFromParsedConfig({ - channels: mockedValidChannels, - providerConnections: mockedProviders, - }), - "default-channel", - ); - - expect(result._unsafeUnwrap()).toBeDefined(); - }); -}); diff --git a/apps/avatax/src/modules/taxes/avatax-webhook-service-factory.ts b/apps/avatax/src/modules/taxes/avatax-webhook-service-factory.ts deleted file mode 100644 index d243d5b3b..000000000 --- a/apps/avatax/src/modules/taxes/avatax-webhook-service-factory.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { err, ok } from "neverthrow"; - -import { AvataxCalculationDateResolver } from "@/modules/avatax/avatax-calculation-date-resolver"; -import { AvataxDocumentCodeResolver } from "@/modules/avatax/avatax-document-code-resolver"; -import { AvataxEntityTypeMatcher } from "@/modules/avatax/avatax-entity-type-matcher"; -import { AvataxCalculateTaxesAdapter } from "@/modules/avatax/calculate-taxes/avatax-calculate-taxes-adapter"; -import { AvataxCalculateTaxesPayloadLinesTransformer } from "@/modules/avatax/calculate-taxes/avatax-calculate-taxes-payload-lines-transformer"; -import { AvataxCalculateTaxesPayloadTransformer } from "@/modules/avatax/calculate-taxes/avatax-calculate-taxes-payload-transformer"; -import { AvataxCalculateTaxesResponseTransformer } from "@/modules/avatax/calculate-taxes/avatax-calculate-taxes-response-transformer"; -import { AvataxCalculateTaxesTaxCodeMatcher } from "@/modules/avatax/calculate-taxes/avatax-calculate-taxes-tax-code-matcher"; -import { AvataxOrderCancelledAdapter } from "@/modules/avatax/order-cancelled/avatax-order-cancelled-adapter"; -import { AvataxOrderCancelledPayloadTransformer } from "@/modules/avatax/order-cancelled/avatax-order-cancelled-payload-transformer"; -import { AvataxOrderConfirmedAdapter } from "@/modules/avatax/order-confirmed/avatax-order-confirmed-adapter"; -import { AvataxOrderConfirmedPayloadService } from "@/modules/avatax/order-confirmed/avatax-order-confirmed-payload.service"; -import { AvataxOrderConfirmedPayloadTransformer } from "@/modules/avatax/order-confirmed/avatax-order-confirmed-payload-transformer"; -import { AvataxOrderConfirmedResponseTransformer } from "@/modules/avatax/order-confirmed/avatax-order-confirmed-response-transformer"; -import { SaleorOrderToAvataxLinesTransformer } from "@/modules/avatax/order-confirmed/saleor-order-to-avatax-lines-transformer"; - -import { BaseError } from "../../error"; -import { AppConfig } from "../../lib/app-config"; -import { AvataxClient } from "../avatax/avatax-client"; -import { AvataxSdkClientFactory } from "../avatax/avatax-sdk-client-factory"; -import { AvataxWebhookService } from "../avatax/avatax-webhook.service"; - -export class AvataxWebhookServiceFactory { - static BrokenConfigurationError = BaseError.subclass("BrokenConfigurationError"); - - static createFromConfig(config: AppConfig, channelSlug: string) { - const channelConfig = config.getConfigForChannelSlug(channelSlug); - - if (channelConfig.isErr()) { - return err( - new this.BrokenConfigurationError( - `Channel config was not found for channel ${channelSlug}`, - { - props: { - channelSlug, - }, - }, - ), - ); - } - - const avaTaxSdk = new AvataxSdkClientFactory().createClient( - channelConfig.value.avataxConfig.config, - ); - const avaTaxClient = new AvataxClient(avaTaxSdk); - - /** - * Compose dependencies - as much as possible lifted as high as possible. - * Next steps of refactor - remove not needed classes and lift them even higher. And inject into use case - */ - const taxProvider = new AvataxWebhookService( - new AvataxCalculateTaxesAdapter(avaTaxClient, new AvataxCalculateTaxesResponseTransformer()), - new AvataxCalculateTaxesPayloadTransformer( - new AvataxCalculateTaxesPayloadLinesTransformer(new AvataxCalculateTaxesTaxCodeMatcher()), - new AvataxEntityTypeMatcher(avaTaxClient), - ), - new AvataxOrderCancelledAdapter(avaTaxClient, new AvataxOrderCancelledPayloadTransformer()), - new AvataxOrderConfirmedAdapter( - avaTaxClient, - new AvataxOrderConfirmedResponseTransformer(), - new AvataxOrderConfirmedPayloadService( - new AvataxOrderConfirmedPayloadTransformer( - new SaleorOrderToAvataxLinesTransformer(), - new AvataxEntityTypeMatcher(avaTaxClient), - new AvataxCalculationDateResolver(), - new AvataxDocumentCodeResolver(), - ), - ), - ), - ); - - return ok({ taxProvider }); - } -} diff --git a/apps/avatax/src/pages/api/webhooks/checkout-calculate-taxes.ts b/apps/avatax/src/pages/api/webhooks/checkout-calculate-taxes.ts index 9e236a12f..ae05c0173 100644 --- a/apps/avatax/src/pages/api/webhooks/checkout-calculate-taxes.ts +++ b/apps/avatax/src/pages/api/webhooks/checkout-calculate-taxes.ts @@ -10,11 +10,11 @@ import { metadataCache, wrapWithMetadataCache } from "@/lib/app-metadata-cache"; import { SubscriptionPayloadErrorChecker } from "@/lib/error-utils"; import { createLogger } from "@/logger"; import { loggerContext } from "@/logger-context"; +import { AvataxCalculateTaxesPayloadLinesTransformer } from "@/modules/avatax/calculate-taxes/avatax-calculate-taxes-payload-lines-transformer"; +import { AvataxCalculateTaxesResponseTransformer } from "@/modules/avatax/calculate-taxes/avatax-calculate-taxes-response-transformer"; +import { AvataxCalculateTaxesTaxCodeMatcher } from "@/modules/avatax/calculate-taxes/avatax-calculate-taxes-tax-code-matcher"; import { CalculateTaxesUseCase } from "@/modules/calculate-taxes/use-case/calculate-taxes.use-case"; -import { clientLogsFeatureConfig } from "@/modules/client-logs/client-logs-feature-config"; -import { DynamoDbLogWriter, ILogWriter, NoopLogWriter } from "@/modules/client-logs/log-writer"; import { LogWriterFactory } from "@/modules/client-logs/log-writer-factory"; -import { LogsRepositoryDynamodb } from "@/modules/client-logs/logs-repository"; import { AvataxInvalidAddressError } from "@/modules/taxes/tax-error"; import { checkoutCalculateTaxesSyncWebhook } from "@/modules/webhooks/definitions/checkout-calculate-taxes"; @@ -32,6 +32,10 @@ const subscriptionErrorChecker = new SubscriptionPayloadErrorChecker(logger, cap const useCase = new CalculateTaxesUseCase({ configExtractor: new AppConfigExtractor(), logWriterFactory: new LogWriterFactory(), + payloadLinesTransformer: new AvataxCalculateTaxesPayloadLinesTransformer( + new AvataxCalculateTaxesTaxCodeMatcher(), + ), + calculateTaxesResponseTransformer: new AvataxCalculateTaxesResponseTransformer(), }); const handler = checkoutCalculateTaxesSyncWebhook.createHandler(async (req, res, ctx) => { diff --git a/apps/avatax/src/pages/api/webhooks/order-calculate-taxes.ts b/apps/avatax/src/pages/api/webhooks/order-calculate-taxes.ts index 387c8d001..a60ef9ffd 100644 --- a/apps/avatax/src/pages/api/webhooks/order-calculate-taxes.ts +++ b/apps/avatax/src/pages/api/webhooks/order-calculate-taxes.ts @@ -1,12 +1,25 @@ +import { AuthData } from "@saleor/app-sdk/APL"; import { wrapWithLoggerContext } from "@saleor/apps-logger/node"; import { withOtel } from "@saleor/apps-otel"; import { ObservabilityAttributes } from "@saleor/apps-otel/src/lib/observability-attributes"; import * as Sentry from "@sentry/nextjs"; import { captureException } from "@sentry/nextjs"; +import { AvataxClient } from "@/modules/avatax/avatax-client"; +import { AvataxConfig } from "@/modules/avatax/avatax-connection-schema"; +import { AvataxEntityTypeMatcher } from "@/modules/avatax/avatax-entity-type-matcher"; +import { AvataxSdkClientFactory } from "@/modules/avatax/avatax-sdk-client-factory"; +import { AvataxCalculateTaxesAdapter } from "@/modules/avatax/calculate-taxes/avatax-calculate-taxes-adapter"; +import { AvataxCalculateTaxesPayloadService } from "@/modules/avatax/calculate-taxes/avatax-calculate-taxes-payload.service"; +import { AvataxCalculateTaxesPayloadLinesTransformer } from "@/modules/avatax/calculate-taxes/avatax-calculate-taxes-payload-lines-transformer"; +import { AvataxCalculateTaxesPayloadTransformer } from "@/modules/avatax/calculate-taxes/avatax-calculate-taxes-payload-transformer"; +import { AvataxCalculateTaxesResponseTransformer } from "@/modules/avatax/calculate-taxes/avatax-calculate-taxes-response-transformer"; +import { AvataxCalculateTaxesTaxCodeMatcher } from "@/modules/avatax/calculate-taxes/avatax-calculate-taxes-tax-code-matcher"; import { AutomaticallyDistributedProductLinesDiscountsStrategy } from "@/modules/avatax/discounts"; +import { AvataxTaxCodeMatchesService } from "@/modules/avatax/tax-code/avatax-tax-code-matches.service"; import { ClientLogStoreRequest } from "@/modules/client-logs/client-log"; import { LogWriterFactory } from "@/modules/client-logs/log-writer-factory"; +import { CalculateTaxesPayload } from "@/modules/webhooks/payloads/calculate-taxes-payload"; import { AppConfigExtractor } from "../../../lib/app-config-extractor"; import { AppConfigurationLogger } from "../../../lib/app-configuration-logger"; @@ -38,6 +51,52 @@ const discountStrategy = new AutomaticallyDistributedProductLinesDiscountsStrate const logsWriterFactory = new LogWriterFactory(); +const createAvataxCalculateTaxesAdapter = (avaTaxClient: AvataxClient) => { + const avataxCalculateTaxesResponseTransformer = new AvataxCalculateTaxesResponseTransformer(); + + return new AvataxCalculateTaxesAdapter(avaTaxClient, avataxCalculateTaxesResponseTransformer); +}; + +const createAvataxCalculateTaxesPayloadTransformer = ( + entityTypeMatcher: AvataxEntityTypeMatcher, +) => { + const avataxCalculateTaxesTaxCodeMatcher = new AvataxCalculateTaxesTaxCodeMatcher(); + const avataxCalculateTaxesPayloadLinesTransformer = + new AvataxCalculateTaxesPayloadLinesTransformer(avataxCalculateTaxesTaxCodeMatcher); + + return new AvataxCalculateTaxesPayloadTransformer( + avataxCalculateTaxesPayloadLinesTransformer, + entityTypeMatcher, + ); +}; + +/** + * @deprecated use CalculateTaxesUseCase instead, see checkout-calculate-taxes handler + */ +async function calculateTaxes( + payload: CalculateTaxesPayload, + avataxConfig: AvataxConfig, + authData: AuthData, + discountStrategy: AutomaticallyDistributedProductLinesDiscountsStrategy, +) { + const avaTaxSdk = new AvataxSdkClientFactory().createClient(avataxConfig); + const avaTaxClient = new AvataxClient(avaTaxSdk); + const calculateTaxesPayloadTransformer = createAvataxCalculateTaxesPayloadTransformer( + new AvataxEntityTypeMatcher(avaTaxClient), + ); + const calculateTaxesAdapter = createAvataxCalculateTaxesAdapter(avaTaxClient); + const payloadService = new AvataxCalculateTaxesPayloadService( + AvataxTaxCodeMatchesService.createFromAuthData(authData), + calculateTaxesPayloadTransformer, + ); + + const avataxModel = await payloadService.getPayload(payload, avataxConfig, discountStrategy); + + const response = await calculateTaxesAdapter.send(avataxModel); + + return response; +} + const handler = orderCalculateTaxesSyncWebhook.createHandler(async (req, res, ctx) => { const logWriter = logsWriterFactory.createWriter(ctx.authData); @@ -123,100 +182,48 @@ const handler = orderCalculateTaxesSyncWebhook.createHandler(async (req, res, ct }); } - const AvataxWebhookServiceFactory = await import( - "../../../modules/taxes/avatax-webhook-service-factory" - ).then((m) => m.AvataxWebhookServiceFactory); - - const avataxWebhookServiceResult = AvataxWebhookServiceFactory.createFromConfig( - config.value, - channelSlug, - ); - - if (avataxWebhookServiceResult.isOk()) { - const { taxProvider } = avataxWebhookServiceResult.value; - const providerConfig = config.value.getConfigForChannelSlug(channelSlug); - - if (providerConfig.isErr()) { - ClientLogStoreRequest.create({ - level: "error", - message: "Taxes not calculated. App faced problem with configuration.", - checkoutOrOrderId: payload.taxBase.sourceObject.id, - channelId: payload.taxBase.channel.slug, - checkoutOrOrder: "order", - }) - .mapErr(captureException) - .map(logWriter.writeLog); - - return res.status(400).json({ - message: `App is not configured properly for order: ${payload.taxBase.sourceObject.id}`, - }); - } - - const calculatedTaxes = await taxProvider.calculateTaxes( - payload, - providerConfig.value.avataxConfig.config, - ctx.authData, - discountStrategy, - ); - - // eslint-disable-next-line @saleor/saleor-app/logger-leak - logger.info("Taxes calculated", { calculatedTaxes: JSON.stringify(calculatedTaxes) }); + const providerConfig = config.value.getConfigForChannelSlug(channelSlug); + if (providerConfig.isErr()) { ClientLogStoreRequest.create({ - level: "info", - message: "Taxes calculated", + level: "error", + message: "Taxes not calculated. App faced problem with configuration.", checkoutOrOrderId: payload.taxBase.sourceObject.id, channelId: payload.taxBase.channel.slug, checkoutOrOrder: "order", - attributes: { - calculatedTaxes: calculatedTaxes, - }, }) .mapErr(captureException) .map(logWriter.writeLog); - return res.status(200).json(ctx.buildResponse(calculatedTaxes)); - } else if (avataxWebhookServiceResult.isErr()) { - const err = avataxWebhookServiceResult.error; - - logger.warn(`Error in taxes calculation occurred: ${err.name} ${err.message}`, { - error: err, + return res.status(400).json({ + message: `App is not configured properly for order: ${payload.taxBase.sourceObject.id}`, }); - - switch (err["constructor"]) { - case AvataxWebhookServiceFactory.BrokenConfigurationError: { - ClientLogStoreRequest.create({ - level: "error", - message: "Taxes not calculated. App faced problem with configuration.", - checkoutOrOrderId: payload.taxBase.sourceObject.id, - channelId: payload.taxBase.channel.slug, - checkoutOrOrder: "order", - }) - .mapErr(captureException) - .map(logWriter.writeLog); - - return res.status(400).json({ - message: `App is not configured properly for order: ${payload.taxBase.sourceObject.id}`, - }); - } - default: { - Sentry.captureException(avataxWebhookServiceResult.error); - logger.error("Unhandled error", { error: err }); - - ClientLogStoreRequest.create({ - level: "error", - message: "Taxes not calculated. Unknown error.", - checkoutOrOrderId: payload.taxBase.sourceObject.id, - channelId: payload.taxBase.channel.slug, - checkoutOrOrder: "order", - }) - .mapErr(captureException) - .map(logWriter.writeLog); - - return res.status(500).json({ message: "Unhandled error" }); - } - } } + + const calculatedTaxes = await calculateTaxes( + payload, + providerConfig.value.avataxConfig.config, + ctx.authData, + discountStrategy, + ); + + // eslint-disable-next-line @saleor/saleor-app/logger-leak + logger.info("Taxes calculated", { calculatedTaxes: JSON.stringify(calculatedTaxes) }); + + ClientLogStoreRequest.create({ + level: "info", + message: "Taxes calculated", + checkoutOrOrderId: payload.taxBase.sourceObject.id, + channelId: payload.taxBase.channel.slug, + checkoutOrOrder: "order", + attributes: { + calculatedTaxes: calculatedTaxes, + }, + }) + .mapErr(captureException) + .map(logWriter.writeLog); + + return res.status(200).json(ctx.buildResponse(calculatedTaxes)); } catch (error) { if (error instanceof AvataxGetTaxError) { logger.warn( diff --git a/apps/avatax/src/pages/api/webhooks/order-cancelled.ts b/apps/avatax/src/pages/api/webhooks/order-cancelled.ts index 09a5fbce9..a328bfc86 100644 --- a/apps/avatax/src/pages/api/webhooks/order-cancelled.ts +++ b/apps/avatax/src/pages/api/webhooks/order-cancelled.ts @@ -5,6 +5,7 @@ import * as Sentry from "@sentry/nextjs"; import { captureException } from "@sentry/nextjs"; import { AvataxOrderCancelledAdapter } from "@/modules/avatax/order-cancelled/avatax-order-cancelled-adapter"; +import { createAvaTaxOrderCancelledAdapterFromConfig } from "@/modules/avatax/order-cancelled/avatax-order-cancelled-adapter-factory"; import { ClientLogStoreRequest } from "@/modules/client-logs/client-log"; import { LogWriterFactory } from "@/modules/client-logs/log-writer-factory"; import { AvataxTransactionAlreadyCancelledError } from "@/modules/taxes/tax-error"; @@ -169,127 +170,83 @@ const handler = orderCancelledAsyncWebhook.createHandler(async (req, res, ctx) = .json({ message: `App configuration is broken for order: ${payload.order?.id}` }); } - const AvataxWebhookServiceFactory = await import( - "../../../modules/taxes/avatax-webhook-service-factory" - ).then((m) => m.AvataxWebhookServiceFactory); - - const avataxWebhookServiceResult = AvataxWebhookServiceFactory.createFromConfig( - config.value, - channelSlug, - ); - logger.info("Cancelling order..."); - if (avataxWebhookServiceResult.isOk()) { - const { taxProvider } = avataxWebhookServiceResult.value; - const providerConfig = config.value.getConfigForChannelSlug(channelSlug); - - if (providerConfig.isErr()) { - ClientLogStoreRequest.create({ - level: "error", - message: "Failed to void order. Broken configuration.", - checkoutOrOrderId: payload.order?.id, - channelId: payload.order?.channel.slug, - checkoutOrOrder: "order", - }) - .mapErr(captureException) - .map(logWriter.writeLog); - - return res - .status(400) - .json({ message: `App is not configured properly for order: ${payload.order?.id}` }); - } + const providerConfig = config.value.getConfigForChannelSlug(channelSlug); - try { - await taxProvider.cancelOrder( - { - avataxId: cancelledOrderInstance.getAvataxId(), - }, - providerConfig.value.avataxConfig.config, - ); - } catch (e) { - // TODO Test once it becomes testable - if (e instanceof AvataxOrderCancelledAdapter.DocumentNotFoundError) { - logger.warn("Document was not found in AvaTax. Responding 400", { - error: e, - }); + if (providerConfig.isErr()) { + ClientLogStoreRequest.create({ + level: "error", + message: "Failed to void order. Broken configuration.", + checkoutOrOrderId: payload.order?.id, + channelId: payload.order?.channel.slug, + checkoutOrOrder: "order", + }) + .mapErr(captureException) + .map(logWriter.writeLog); - return res.status(400).send({ - message: "AvaTax responded with DocumentNotFound. Please consult AvaTax docs", - }); - } + return res + .status(400) + .json({ message: `App is not configured properly for order: ${payload.order?.id}` }); + } - if (e instanceof AvataxTransactionAlreadyCancelledError) { - logger.warn("Transaction was already cancelled in AvaTax. Responding 200", { - error: e, - }); + const avaTaxOrderCancelledAdapter = createAvaTaxOrderCancelledAdapterFromConfig( + providerConfig.value.avataxConfig.config, + ); - return res.status(200).send({ - message: "Transaction was already cancelled in AvaTax", - }); - } + try { + await avaTaxOrderCancelledAdapter.send( + { + avataxId: cancelledOrderInstance.getAvataxId(), + }, + providerConfig.value.avataxConfig.config, + ); + } catch (e) { + // TODO Test once it becomes testable + if (e instanceof AvataxOrderCancelledAdapter.DocumentNotFoundError) { + logger.warn("Document was not found in AvaTax. Responding 400", { + error: e, + }); + + return res.status(400).send({ + message: "AvaTax responded with DocumentNotFound. Please consult AvaTax docs", + }); + } - ClientLogStoreRequest.create({ - level: "error", - message: "Failed to void order. AvaTax returned error.", - checkoutOrOrder: "order", - checkoutOrOrderId: payload.order?.id, - channelId: payload.order?.channel.slug, - }) - .mapErr(captureException) - .map(logWriter.writeLog); + if (e instanceof AvataxTransactionAlreadyCancelledError) { + logger.warn("Transaction was already cancelled in AvaTax. Responding 200", { + error: e, + }); + + return res.status(200).send({ + message: "Transaction was already cancelled in AvaTax", + }); } ClientLogStoreRequest.create({ - level: "info", - message: "Order voided in AvaTax", + level: "error", + message: "Failed to void order. AvaTax returned error.", checkoutOrOrder: "order", checkoutOrOrderId: payload.order?.id, channelId: payload.order?.channel.slug, }) .mapErr(captureException) .map(logWriter.writeLog); - - logger.info("Order cancelled"); - - return res.status(200).end(); } - if (avataxWebhookServiceResult.isErr()) { - logger.error("Tax provider couldn't cancel the order:", avataxWebhookServiceResult.error); + ClientLogStoreRequest.create({ + level: "info", + message: "Order voided in AvaTax", + checkoutOrOrder: "order", + checkoutOrOrderId: payload.order?.id, + channelId: payload.order?.channel.slug, + }) + .mapErr(captureException) + .map(logWriter.writeLog); - switch (avataxWebhookServiceResult.error["constructor"]) { - case AvataxWebhookServiceFactory.BrokenConfigurationError: { - ClientLogStoreRequest.create({ - level: "error", - message: "Failed to void order. Broken configuration.", - checkoutOrOrderId: payload.order?.id, - checkoutOrOrder: "order", - channelId: payload.order?.channel.slug, - }) - .mapErr(captureException) - .map(logWriter.writeLog); + logger.info("Order cancelled"); - return res.status(400).json({ message: "App is not configured properly." }); - } - default: { - Sentry.captureException(avataxWebhookServiceResult.error); - logger.fatal("Unhandled error", { error: avataxWebhookServiceResult.error }); - - ClientLogStoreRequest.create({ - level: "error", - message: "Failed to void order. Unhandled error", - checkoutOrOrderId: payload.order?.id, - checkoutOrOrder: "order", - channelId: payload.order?.channel.slug, - }) - .mapErr(captureException) - .map(logWriter.writeLog); - - return res.status(500).json({ message: "Unhandled error" }); - } - } - } + return res.status(200).end(); }); export default wrapWithLoggerContext( diff --git a/apps/avatax/src/pages/api/webhooks/order-confirmed.ts b/apps/avatax/src/pages/api/webhooks/order-confirmed.ts index f4d18a486..d591a73b2 100644 --- a/apps/avatax/src/pages/api/webhooks/order-confirmed.ts +++ b/apps/avatax/src/pages/api/webhooks/order-confirmed.ts @@ -1,10 +1,13 @@ +import { AuthData } from "@saleor/app-sdk/APL"; import { wrapWithLoggerContext } from "@saleor/apps-logger/node"; import { withOtel } from "@saleor/apps-otel"; import { ObservabilityAttributes } from "@saleor/apps-otel/src/lib/observability-attributes"; import * as Sentry from "@sentry/nextjs"; import { captureException } from "@sentry/nextjs"; +import { AvataxConfig } from "@/modules/avatax/avatax-connection-schema"; import { PriceReductionDiscountsStrategy } from "@/modules/avatax/discounts"; +import { createAvaTaxOrderConfirmedAdapterFromAvaTaxConfig } from "@/modules/avatax/order-confirmed/avatax-order-confirmed-adapter-factory"; import { ClientLogStoreRequest } from "@/modules/client-logs/client-log"; import { LogWriterFactory } from "@/modules/client-logs/log-writer-factory"; @@ -38,6 +41,28 @@ const discountStrategy = new PriceReductionDiscountsStrategy(); const logsWriterFactory = new LogWriterFactory(); +/** + * In the future this should be part of the use-case + */ +async function confirmOrder( + confirmedOrderEvent: SaleorOrderConfirmedEvent, + avataxConfig: AvataxConfig, + authData: AuthData, + discountStrategy: PriceReductionDiscountsStrategy, +) { + const avataxOrderConfirmedAdapter = + createAvaTaxOrderConfirmedAdapterFromAvaTaxConfig(avataxConfig); + + const response = await avataxOrderConfirmedAdapter.send( + { confirmedOrderEvent }, + avataxConfig, + authData, + discountStrategy, + ); + + return response; +} + const handler = orderConfirmedAsyncWebhook.createHandler(async (req, res, ctx) => { const { payload, authData } = ctx; @@ -165,153 +190,73 @@ const handler = orderConfirmedAsyncWebhook.createHandler(async (req, res, ctx) = metadataCache.setMetadata(appMetadata); - const AvataxWebhookServiceFactory = await import( - "../../../modules/taxes/avatax-webhook-service-factory" - ).then((m) => m.AvataxWebhookServiceFactory); + logger.debug("Confirming order..."); - const webhookServiceResult = AvataxWebhookServiceFactory.createFromConfig( - config.value, + const providerConfig = config.value.getConfigForChannelSlug( confirmedOrderEvent.getChannelSlug(), ); - logger.debug("Confirming order..."); + if (providerConfig.isErr()) { + ClientLogStoreRequest.create({ + level: "error", + message: "Failed to commit order. Configuration error.", + checkoutOrOrderId: payload.order?.id, + checkoutOrOrder: "order", + channelId: payload.order?.channel.slug, + }) + .mapErr(captureException) + .map(logWriter.writeLog); + + return res.status(400).json({ + message: `App is not configured properly for order: ${payload.order?.id}`, + }); + } - if (webhookServiceResult.isOk()) { - const { taxProvider } = webhookServiceResult.value; - const providerConfig = config.value.getConfigForChannelSlug( - confirmedOrderEvent.getChannelSlug(), + try { + const confirmedOrder = await confirmOrder( + confirmedOrderEvent, + providerConfig.value.avataxConfig.config, + ctx.authData, + discountStrategy, ); - if (providerConfig.isErr()) { - ClientLogStoreRequest.create({ - level: "error", - message: "Failed to commit order. Configuration error.", - checkoutOrOrderId: payload.order?.id, - checkoutOrOrder: "order", - channelId: payload.order?.channel.slug, - }) - .mapErr(captureException) - .map(logWriter.writeLog); - - return res.status(400).json({ - message: `App is not configured properly for order: ${payload.order?.id}`, - }); - } + logger.info("Order confirmed", { orderId: confirmedOrder.id }); - try { - const confirmedOrder = await taxProvider.confirmOrder( - confirmedOrderEvent, - providerConfig.value.avataxConfig.config, - ctx.authData, - discountStrategy, - ); - - logger.info("Order confirmed", { orderId: confirmedOrder.id }); - - const client = createInstrumentedGraphqlClient({ - saleorApiUrl, - token, - }); - - const orderMetadataManager = new OrderMetadataManager(client); - - await orderMetadataManager.updateOrderMetadataWithExternalId( - confirmedOrderEvent.getOrderId(), - confirmedOrder.id, - ); - logger.info("Updated order metadata with externalId"); - - ClientLogStoreRequest.create({ - level: "info", - message: "Order committed successfully", - checkoutOrOrderId: payload.order?.id, - checkoutOrOrder: "order", - channelId: payload.order?.channel.slug, - attributes: { - confirmedOrderId: confirmedOrder.id, - }, - }) - .mapErr(captureException) - .map(logWriter.writeLog); - - return res.status(200).end(); - } catch (error) { - logger.debug("Error confirming order", { error: error }); - - switch (true) { - case error instanceof TaxBadPayloadError: { - ClientLogStoreRequest.create({ - level: "error", - message: "Failed to commit order. Webhook payload invalid", - checkoutOrOrderId: payload.order?.id, - checkoutOrOrder: "order", - channelId: payload.order?.channel.slug, - }) - .mapErr(captureException) - .map(logWriter.writeLog); - - return res - .status(400) - .json({ message: `Order: ${payload.order?.id} data is not valid` }); - } - case error instanceof AvataxStringLengthError: { - ClientLogStoreRequest.create({ - level: "error", - message: `Failed to commit order: ${error?.description} `, - checkoutOrOrderId: payload.order?.id, - checkoutOrOrder: "order", - channelId: payload.order?.channel.slug, - }) - .mapErr(captureException) - .map(logWriter.writeLog); - - return res.status(400).json({ - message: `AvaTax service returned validation error: ${error?.description}`, - }); - } - case error instanceof AvataxEntityNotFoundError: { - ClientLogStoreRequest.create({ - level: "error", - message: `Failed to commit order: ${error?.description} `, - checkoutOrOrderId: payload.order?.id, - checkoutOrOrder: "order", - channelId: payload.order?.channel.slug, - }) - .mapErr(captureException) - .map(logWriter.writeLog); - - return res.status(400).json({ - message: `AvaTax service returned validation error: ${error?.description}`, - }); - } - } - Sentry.captureException(error); - logger.error("Unhandled error executing webhook", { error: error }); - - ClientLogStoreRequest.create({ - level: "error", - message: `Failed to commit order: Unhandled error `, - checkoutOrOrderId: payload.order?.id, - checkoutOrOrder: "order", - channelId: payload.order?.channel.slug, - }) - .mapErr(captureException) - .map(logWriter.writeLog); - - return res.status(500).json({ message: "Unhandled error" }); - } - } + const client = createInstrumentedGraphqlClient({ + saleorApiUrl, + token, + }); - if (webhookServiceResult.isErr()) { - const error = webhookServiceResult.error; + const orderMetadataManager = new OrderMetadataManager(client); - logger.debug("Error confirming order", { error }); + await orderMetadataManager.updateOrderMetadataWithExternalId( + confirmedOrderEvent.getOrderId(), + confirmedOrder.id, + ); + logger.info("Updated order metadata with externalId"); - switch (error["constructor"]) { - case AvataxWebhookServiceFactory.BrokenConfigurationError: { + ClientLogStoreRequest.create({ + level: "info", + message: "Order committed successfully", + checkoutOrOrderId: payload.order?.id, + checkoutOrOrder: "order", + channelId: payload.order?.channel.slug, + attributes: { + confirmedOrderId: confirmedOrder.id, + }, + }) + .mapErr(captureException) + .map(logWriter.writeLog); + + return res.status(200).end(); + } catch (error) { + logger.debug("Error confirming order", { error: error }); + + switch (true) { + case error instanceof TaxBadPayloadError: { ClientLogStoreRequest.create({ level: "error", - message: `Failed to commit order: Broken configuration `, + message: "Failed to commit order. Webhook payload invalid", checkoutOrOrderId: payload.order?.id, checkoutOrOrder: "order", channelId: payload.order?.channel.slug, @@ -319,15 +264,27 @@ const handler = orderConfirmedAsyncWebhook.createHandler(async (req, res, ctx) = .mapErr(captureException) .map(logWriter.writeLog); - return res.status(400).json({ message: "App is not configured properly." }); + return res.status(400).json({ message: `Order: ${payload.order?.id} data is not valid` }); } - default: { - Sentry.captureException(webhookServiceResult.error); - logger.fatal("Unhandled error", { error }); + case error instanceof AvataxStringLengthError: { + ClientLogStoreRequest.create({ + level: "error", + message: `Failed to commit order: ${error?.description} `, + checkoutOrOrderId: payload.order?.id, + checkoutOrOrder: "order", + channelId: payload.order?.channel.slug, + }) + .mapErr(captureException) + .map(logWriter.writeLog); + return res.status(400).json({ + message: `AvaTax service returned validation error: ${error?.description}`, + }); + } + case error instanceof AvataxEntityNotFoundError: { ClientLogStoreRequest.create({ level: "error", - message: `Failed to commit order: Unhandled error `, + message: `Failed to commit order: ${error?.description} `, checkoutOrOrderId: payload.order?.id, checkoutOrOrder: "order", channelId: payload.order?.channel.slug, @@ -335,9 +292,25 @@ const handler = orderConfirmedAsyncWebhook.createHandler(async (req, res, ctx) = .mapErr(captureException) .map(logWriter.writeLog); - return res.status(500).json({ message: "Unhandled error" }); + return res.status(400).json({ + message: `AvaTax service returned validation error: ${error?.description}`, + }); } } + Sentry.captureException(error); + logger.error("Unhandled error executing webhook", { error: error }); + + ClientLogStoreRequest.create({ + level: "error", + message: `Failed to commit order: Unhandled error `, + checkoutOrOrderId: payload.order?.id, + checkoutOrOrder: "order", + channelId: payload.order?.channel.slug, + }) + .mapErr(captureException) + .map(logWriter.writeLog); + + return res.status(500).json({ message: "Unhandled error" }); } } catch (error) { Sentry.captureException(error);