diff --git a/.changeset/unlucky-tips-smash.md b/.changeset/unlucky-tips-smash.md new file mode 100644 index 0000000..532c35a --- /dev/null +++ b/.changeset/unlucky-tips-smash.md @@ -0,0 +1,5 @@ +--- +"saleor-app-taxes": patch +--- + +Fixed the error when checkout couldn't calculate taxes when no customerCode was provided. In calculate taxes, the customerCode is now derived from issuingPrincipal's id. diff --git a/apps/taxes/graphql/fragments/CalculateTaxesEvent.graphql b/apps/taxes/graphql/fragments/CalculateTaxesEvent.graphql index 08c11e0..0273ede 100644 --- a/apps/taxes/graphql/fragments/CalculateTaxesEvent.graphql +++ b/apps/taxes/graphql/fragments/CalculateTaxesEvent.graphql @@ -10,5 +10,11 @@ fragment CalculateTaxesEvent on Event { value } } + issuingPrincipal { + __typename + ... on User { + id + } + } } } diff --git a/apps/taxes/graphql/fragments/TaxBase.graphql b/apps/taxes/graphql/fragments/TaxBase.graphql index 2c28eb7..0a0efbf 100644 --- a/apps/taxes/graphql/fragments/TaxBase.graphql +++ b/apps/taxes/graphql/fragments/TaxBase.graphql @@ -62,15 +62,9 @@ fragment TaxBase on TaxableObject { sourceObject { ... on Checkout { avataxEntityCode: metafield(key: "avataxEntityCode") - user { - id - } } ... on Order { avataxEntityCode: metafield(key: "avataxEntityCode") - user { - id - } } } } diff --git a/apps/taxes/src/modules/avatax/avatax-webhook.service.ts b/apps/taxes/src/modules/avatax/avatax-webhook.service.ts index c53f768..95a46aa 100644 --- a/apps/taxes/src/modules/avatax/avatax-webhook.service.ts +++ b/apps/taxes/src/modules/avatax/avatax-webhook.service.ts @@ -8,6 +8,7 @@ import { AvataxConfig, defaultAvataxConfig } from "./avatax-connection-schema"; import { AvataxCalculateTaxesAdapter } from "./calculate-taxes/avatax-calculate-taxes-adapter"; import { AvataxOrderCancelledAdapter } from "./order-cancelled/avatax-order-cancelled-adapter"; import { AvataxOrderConfirmedAdapter } from "./order-confirmed/avatax-order-confirmed-adapter"; +import { CalculateTaxesPayload } from "../../pages/api/webhooks/checkout-calculate-taxes"; export class AvataxWebhookService implements ProviderWebhookService { config = defaultAvataxConfig; @@ -27,10 +28,10 @@ export class AvataxWebhookService implements ProviderWebhookService { this.client = avataxClient; } - async calculateTaxes(taxBase: TaxBaseFragment) { + async calculateTaxes(payload: CalculateTaxesPayload) { const adapter = new AvataxCalculateTaxesAdapter(this.config, this.authData); - const response = await adapter.send({ taxBase }); + const response = await adapter.send(payload); return response; } diff --git a/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-adapter.ts b/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-adapter.ts index 02d7a3e..5435a31 100644 --- a/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-adapter.ts +++ b/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-adapter.ts @@ -1,6 +1,6 @@ import { AuthData } from "@saleor/app-sdk/APL"; -import { TaxBaseFragment } from "../../../../generated/graphql"; import { Logger, createLogger } from "../../../lib/logger"; +import { CalculateTaxesPayload } from "../../../pages/api/webhooks/checkout-calculate-taxes"; import { CalculateTaxesResponse } from "../../taxes/tax-provider-webhook"; import { WebhookAdapter } from "../../taxes/tax-webhook-adapter"; import { AvataxClient, CreateTransactionArgs } from "../avatax-client"; @@ -10,26 +10,27 @@ import { AvataxCalculateTaxesResponseTransformer } from "./avatax-calculate-taxe export const SHIPPING_ITEM_CODE = "Shipping"; -export type AvataxCalculateTaxesPayload = { - taxBase: TaxBaseFragment; -}; - export type AvataxCalculateTaxesTarget = CreateTransactionArgs; export type AvataxCalculateTaxesResponse = CalculateTaxesResponse; export class AvataxCalculateTaxesAdapter - implements WebhookAdapter + implements WebhookAdapter { private logger: Logger; - constructor(private readonly config: AvataxConfig, private authData: AuthData) { + constructor( + private readonly config: AvataxConfig, + private authData: AuthData, + ) { this.logger = createLogger({ name: "AvataxCalculateTaxesAdapter" }); } - // todo: refactor because its getting too big - async send(payload: AvataxCalculateTaxesPayload): Promise { - this.logger.debug("Transforming the Saleor payload for calculating taxes with AvaTax..."); + async send(payload: CalculateTaxesPayload): Promise { + this.logger.debug( + { payload }, + "Transforming the Saleor payload for calculating taxes with AvaTax...", + ); const payloadService = new AvataxCalculateTaxesPayloadService(this.authData); - const target = await payloadService.getPayload(payload.taxBase, this.config); + const target = await payloadService.getPayload(payload, this.config); this.logger.debug("Calling AvaTax createTransaction with transformed payload..."); diff --git a/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-mock-generator.ts b/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-mock-generator.ts index 4e7797a..67955a4 100644 --- a/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-mock-generator.ts +++ b/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-mock-generator.ts @@ -107,9 +107,6 @@ const defaultTaxBase: TaxBase = { ], sourceObject: { avataxEntityCode: null, - user: { - id: "VXNlcjoyMDg0NTEwNDEw", - }, }, }; diff --git a/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-payload-transformer.test.ts b/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-payload-transformer.test.ts index de4729f..1612af1 100644 --- a/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-payload-transformer.test.ts +++ b/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-payload-transformer.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest"; import { AvataxCalculateTaxesMockGenerator } from "./avatax-calculate-taxes-mock-generator"; import { AvataxCalculateTaxesPayloadTransformer } from "./avatax-calculate-taxes-payload-transformer"; import { DocumentType } from "avatax/lib/enums/DocumentType"; +import { CalculateTaxesPayload } from "../../../pages/api/webhooks/checkout-calculate-taxes"; const mockGenerator = new AvataxCalculateTaxesMockGenerator(); const avataxConfigMock = mockGenerator.generateAvataxConfig(); @@ -10,11 +11,18 @@ describe("AvataxCalculateTaxesPayloadTransformer", () => { it("returns document type of SalesInvoice", async () => { const taxBaseMock = mockGenerator.generateTaxBase(); const matchesMock = mockGenerator.generateTaxCodeMatches(); + const payloadMock = { + taxBase: taxBaseMock, + issuingPrincipal: { + __typename: "User", + id: "1", + }, + } as unknown as CalculateTaxesPayload; const payload = await new AvataxCalculateTaxesPayloadTransformer().transform( - taxBaseMock, + payloadMock, avataxConfigMock, - matchesMock + matchesMock, ); expect(payload.model.type).toBe(DocumentType.SalesOrder); @@ -22,27 +30,56 @@ describe("AvataxCalculateTaxesPayloadTransformer", () => { it("when discounts, calculates the sum of discounts", async () => { const taxBaseMock = mockGenerator.generateTaxBase({ discounts: [{ amount: { amount: 10 } }] }); const matchesMock = mockGenerator.generateTaxCodeMatches(); + const payloadMock = { + taxBase: taxBaseMock, + issuingPrincipal: { + __typename: "User", + id: "1", + }, + } as unknown as CalculateTaxesPayload; const payload = await new AvataxCalculateTaxesPayloadTransformer().transform( - taxBaseMock, + payloadMock, avataxConfigMock, - matchesMock + matchesMock, ); expect(payload.model.discount).toEqual(10); }); it("when no discounts, the sum of discount is 0", async () => { - const mockGenerator = new AvataxCalculateTaxesMockGenerator(); - const avataxConfigMock = mockGenerator.generateAvataxConfig(); const taxBaseMock = mockGenerator.generateTaxBase(); const matchesMock = mockGenerator.generateTaxCodeMatches(); + const payloadMock = { + taxBase: taxBaseMock, + issuingPrincipal: { + __typename: "User", + id: "1", + }, + } as unknown as CalculateTaxesPayload; + const payload = await new AvataxCalculateTaxesPayloadTransformer().transform( - taxBaseMock, + payloadMock, avataxConfigMock, - matchesMock + matchesMock, ); expect(payload.model.discount).toEqual(0); }); + it("when no issuingPrincipal.id, throws an error", async () => { + const taxBaseMock = mockGenerator.generateTaxBase(); + const matchesMock = mockGenerator.generateTaxCodeMatches(); + + const payloadMock = { + taxBase: taxBaseMock, + } as unknown as CalculateTaxesPayload; + + await expect( + new AvataxCalculateTaxesPayloadTransformer().transform( + payloadMock, + avataxConfigMock, + matchesMock, + ), + ).rejects.toThrow("This field must be defined."); + }); }); diff --git a/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-payload-transformer.ts b/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-payload-transformer.ts index 97f4c9e..1eb5e99 100644 --- a/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-payload-transformer.ts +++ b/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-payload-transformer.ts @@ -7,6 +7,8 @@ import { AvataxConfig, defaultAvataxConfig } from "../avatax-connection-schema"; import { AvataxTaxCodeMatches } from "../tax-code/avatax-tax-code-match-repository"; import { AvataxCalculateTaxesPayloadLinesTransformer } from "./avatax-calculate-taxes-payload-lines-transformer"; import { AvataxEntityTypeMatcher } from "../avatax-entity-type-matcher"; +import { taxProviderUtils } from "../../taxes/tax-provider-utils"; +import { CalculateTaxesPayload } from "../../../pages/api/webhooks/checkout-calculate-taxes"; export class AvataxCalculateTaxesPayloadTransformer { private matchDocumentType(config: AvataxConfig): DocumentType { @@ -20,32 +22,38 @@ export class AvataxCalculateTaxesPayloadTransformer { } async transform( - taxBase: TaxBaseFragment, + payload: CalculateTaxesPayload, avataxConfig: AvataxConfig, - matches: AvataxTaxCodeMatches + matches: AvataxTaxCodeMatches, ): Promise { const payloadLinesTransformer = new AvataxCalculateTaxesPayloadLinesTransformer(); const avataxClient = new AvataxClient(avataxConfig); const entityTypeMatcher = new AvataxEntityTypeMatcher({ client: avataxClient }); - const entityUseCode = await entityTypeMatcher.match(taxBase.sourceObject.avataxEntityCode); + const entityUseCode = await entityTypeMatcher.match( + payload.taxBase.sourceObject.avataxEntityCode, + ); + + const customerCode = taxProviderUtils.resolveStringOrThrow( + payload.issuingPrincipal?.__typename === "User" ? payload.issuingPrincipal.id : undefined, + ); return { model: { type: this.matchDocumentType(avataxConfig), entityUseCode, - customerCode: taxBase.sourceObject.user?.id ?? "", + customerCode, companyCode: avataxConfig.companyCode ?? defaultAvataxConfig.companyCode, // * commit: If true, the transaction will be committed immediately after it is created. See: https://developer.avalara.com/communications/dev-guide_rest_v2/commit-uncommit commit: avataxConfig.isAutocommit, addresses: { shipFrom: avataxAddressFactory.fromChannelAddress(avataxConfig.address), - shipTo: avataxAddressFactory.fromSaleorAddress(taxBase.address!), + shipTo: avataxAddressFactory.fromSaleorAddress(payload.taxBase.address!), }, - currencyCode: taxBase.currency, - lines: payloadLinesTransformer.transform(taxBase, avataxConfig, matches), + currencyCode: payload.taxBase.currency, + lines: payloadLinesTransformer.transform(payload.taxBase, avataxConfig, matches), date: new Date(), discount: discountUtils.sumDiscounts( - taxBase.discounts.map((discount) => discount.amount.amount) + payload.taxBase.discounts.map((discount) => discount.amount.amount), ), }, }; diff --git a/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-payload.service.ts b/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-payload.service.ts index 76a9fbe..e3dcc57 100644 --- a/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-payload.service.ts +++ b/apps/taxes/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-payload.service.ts @@ -4,6 +4,7 @@ import { CreateTransactionArgs } from "../avatax-client"; import { AvataxConfig } from "../avatax-connection-schema"; import { AvataxTaxCodeMatchesService } from "../tax-code/avatax-tax-code-matches.service"; import { AvataxCalculateTaxesPayloadTransformer } from "./avatax-calculate-taxes-payload-transformer"; +import { CalculateTaxesPayload } from "../../../pages/api/webhooks/checkout-calculate-taxes"; export class AvataxCalculateTaxesPayloadService { constructor(private authData: AuthData) {} @@ -15,12 +16,12 @@ export class AvataxCalculateTaxesPayloadService { } async getPayload( - taxBase: TaxBaseFragment, - avataxConfig: AvataxConfig + payload: CalculateTaxesPayload, + avataxConfig: AvataxConfig, ): Promise { const matches = await this.getMatches(); const payloadTransformer = new AvataxCalculateTaxesPayloadTransformer(); - return payloadTransformer.transform(taxBase, avataxConfig, matches); + return payloadTransformer.transform(payload, avataxConfig, matches); } } diff --git a/apps/taxes/src/modules/avatax/order-confirmed/avatax-order-confirmed-payload-transformer.ts b/apps/taxes/src/modules/avatax/order-confirmed/avatax-order-confirmed-payload-transformer.ts index a694257..469e9f8 100644 --- a/apps/taxes/src/modules/avatax/order-confirmed/avatax-order-confirmed-payload-transformer.ts +++ b/apps/taxes/src/modules/avatax/order-confirmed/avatax-order-confirmed-payload-transformer.ts @@ -25,7 +25,7 @@ export class AvataxOrderConfirmedPayloadTransformer { async transform( order: OrderConfirmedSubscriptionFragment, avataxConfig: AvataxConfig, - matches: AvataxTaxCodeMatches + matches: AvataxTaxCodeMatches, ): Promise { const avataxClient = new AvataxClient(avataxConfig); @@ -46,9 +46,7 @@ export class AvataxOrderConfirmedPayloadTransformer { code, type: this.matchDocumentType(avataxConfig), entityUseCode, - customerCode: - order.user?.id ?? - "" /* In Saleor AvaTax plugin, the customer code is 0. In Taxes App, we set it to the user id. */, + customerCode: taxProviderUtils.resolveStringOrThrow(order.user?.id), companyCode: avataxConfig.companyCode ?? defaultAvataxConfig.companyCode, // * commit: If true, the transaction will be committed immediately after it is created. See: https://developer.avalara.com/communications/dev-guide_rest_v2/commit-uncommit commit: avataxConfig.isAutocommit, @@ -62,7 +60,7 @@ export class AvataxOrderConfirmedPayloadTransformer { lines: linesTransformer.transform(order, avataxConfig, matches), date, discount: discountUtils.sumDiscounts( - order.discounts.map((discount) => discount.amount.amount) + order.discounts.map((discount) => discount.amount.amount), ), }, }; diff --git a/apps/taxes/src/modules/taxes/get-active-connection-service.ts b/apps/taxes/src/modules/taxes/get-active-connection-service.ts index 24c4cd2..35fb7e5 100644 --- a/apps/taxes/src/modules/taxes/get-active-connection-service.ts +++ b/apps/taxes/src/modules/taxes/get-active-connection-service.ts @@ -12,6 +12,7 @@ import { AvataxWebhookService } from "../avatax/avatax-webhook.service"; import { ProviderConnection } from "../provider-connections/provider-connections"; import { TaxJarWebhookService } from "../taxjar/taxjar-webhook.service"; import { ProviderWebhookService } from "./tax-provider-webhook"; +import { CalculateTaxesPayload } from "../../pages/api/webhooks/checkout-calculate-taxes"; // todo: refactor to a factory class ActiveTaxProviderService implements ProviderWebhookService { @@ -47,7 +48,7 @@ class ActiveTaxProviderService implements ProviderWebhookService { } } - async calculateTaxes(payload: TaxBaseFragment) { + async calculateTaxes(payload: CalculateTaxesPayload) { return this.client.calculateTaxes(payload); } diff --git a/apps/taxes/src/modules/taxes/tax-provider-utils.ts b/apps/taxes/src/modules/taxes/tax-provider-utils.ts index 31f81b6..5e12986 100644 --- a/apps/taxes/src/modules/taxes/tax-provider-utils.ts +++ b/apps/taxes/src/modules/taxes/tax-provider-utils.ts @@ -16,7 +16,10 @@ function resolveOptionalOrThrow(value: T | undefined | null, error?: Error): } function resolveStringOrThrow(value: string | undefined | null): string { - return z.string().min(1, { message: "This field can not be empty." }).parse(value); + return z + .string({ required_error: "This field must be defined." }) + .min(1, { message: "This field can not be empty." }) + .parse(value); } export const taxProviderUtils = { diff --git a/apps/taxes/src/modules/taxes/tax-provider-webhook.ts b/apps/taxes/src/modules/taxes/tax-provider-webhook.ts index 78bbf69..9c08236 100644 --- a/apps/taxes/src/modules/taxes/tax-provider-webhook.ts +++ b/apps/taxes/src/modules/taxes/tax-provider-webhook.ts @@ -1,5 +1,6 @@ import { SyncWebhookResponsesMap } from "@saleor/app-sdk/handlers/next"; -import { OrderConfirmedSubscriptionFragment, TaxBaseFragment } from "../../../generated/graphql"; +import { OrderConfirmedSubscriptionFragment } from "../../../generated/graphql"; +import { CalculateTaxesPayload } from "../../pages/api/webhooks/checkout-calculate-taxes"; import { OrderCancelledPayload } from "../../pages/api/webhooks/order-cancelled"; export type CalculateTaxesResponse = SyncWebhookResponsesMap["ORDER_CALCULATE_TAXES"]; @@ -7,7 +8,7 @@ export type CalculateTaxesResponse = SyncWebhookResponsesMap["ORDER_CALCULATE_TA export type CreateOrderResponse = { id: string }; export interface ProviderWebhookService { - calculateTaxes: (payload: TaxBaseFragment) => Promise; + calculateTaxes: (payload: CalculateTaxesPayload) => Promise; confirmOrder: (payload: OrderConfirmedSubscriptionFragment) => Promise; cancelOrder: (payload: OrderCancelledPayload) => Promise; } diff --git a/apps/taxes/src/modules/taxjar/calculate-taxes/taxjar-calculate-taxes-mock-generator.ts b/apps/taxes/src/modules/taxjar/calculate-taxes/taxjar-calculate-taxes-mock-generator.ts index 352f50a..8d80ffd 100644 --- a/apps/taxes/src/modules/taxjar/calculate-taxes/taxjar-calculate-taxes-mock-generator.ts +++ b/apps/taxes/src/modules/taxjar/calculate-taxes/taxjar-calculate-taxes-mock-generator.ts @@ -96,9 +96,6 @@ const taxIncludedTaxBase: TaxBase = { ], sourceObject: { avataxEntityCode: null, - user: { - id: "VXNlcjoyMDg0NTEwNDEw", - }, }, }; @@ -194,9 +191,6 @@ const taxExcludedTaxBase: TaxBase = { ], sourceObject: { avataxEntityCode: null, - user: { - id: "VXNlcjoyMDg0NTEwNDEw", - }, }, }; diff --git a/apps/taxes/src/modules/taxjar/taxjar-webhook.service.ts b/apps/taxes/src/modules/taxjar/taxjar-webhook.service.ts index e160a19..3a742bb 100644 --- a/apps/taxes/src/modules/taxjar/taxjar-webhook.service.ts +++ b/apps/taxes/src/modules/taxjar/taxjar-webhook.service.ts @@ -10,6 +10,7 @@ import { TaxJarCalculateTaxesAdapter } from "./calculate-taxes/taxjar-calculate- import { TaxJarOrderConfirmedAdapter } from "./order-confirmed/taxjar-order-confirmed-adapter"; import { TaxJarClient } from "./taxjar-client"; import { TaxJarConfig } from "./taxjar-connection-schema"; +import { CalculateTaxesPayload } from "../../pages/api/webhooks/checkout-calculate-taxes"; export class TaxJarWebhookService implements ProviderWebhookService { client: TaxJarClient; @@ -29,10 +30,10 @@ export class TaxJarWebhookService implements ProviderWebhookService { }); } - async calculateTaxes(taxBase: TaxBaseFragment) { + async calculateTaxes(payload: CalculateTaxesPayload) { const adapter = new TaxJarCalculateTaxesAdapter(this.config, this.authData); - const response = await adapter.send({ taxBase }); + const response = await adapter.send(payload); return response; } 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 af9bd3e..092b244 100644 --- a/apps/taxes/src/pages/api/webhooks/checkout-calculate-taxes.ts +++ b/apps/taxes/src/pages/api/webhooks/checkout-calculate-taxes.ts @@ -14,7 +14,10 @@ export const config = { }, }; -type CalculateTaxesPayload = Extract; +export type CalculateTaxesPayload = Extract< + CalculateTaxesEventFragment, + { __typename: "CalculateTaxes" } +>; function verifyCalculateTaxesPayload(payload: CalculateTaxesPayload) { if (!payload.taxBase.lines) { @@ -52,11 +55,11 @@ export default checkoutCalculateTaxesSyncWebhook.createHandler(async (req, res, const activeConnectionService = getActiveConnectionService( channelSlug, appMetadata, - ctx.authData + ctx.authData, ); logger.info("Found active connection service. Calculating taxes..."); - const calculatedTaxes = await activeConnectionService.calculateTaxes(payload.taxBase); + const calculatedTaxes = await activeConnectionService.calculateTaxes(payload); logger.info({ calculatedTaxes }, "Taxes calculated"); return webhookResponse.success(ctx.buildResponse(calculatedTaxes)); 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 e8ed2ed..9fe1348 100644 --- a/apps/taxes/src/pages/api/webhooks/order-calculate-taxes.ts +++ b/apps/taxes/src/pages/api/webhooks/order-calculate-taxes.ts @@ -52,11 +52,11 @@ export default orderCalculateTaxesSyncWebhook.createHandler(async (req, res, ctx const activeConnectionService = getActiveConnectionService( channelSlug, appMetadata, - ctx.authData + ctx.authData, ); logger.info("Found active connection service. Calculating taxes..."); - const calculatedTaxes = await activeConnectionService.calculateTaxes(payload.taxBase); + const calculatedTaxes = await activeConnectionService.calculateTaxes(payload); logger.info({ calculatedTaxes }, "Taxes calculated"); return webhookResponse.success(ctx.buildResponse(calculatedTaxes));