diff --git a/.changeset/fifty-radios-beam.md b/.changeset/fifty-radios-beam.md new file mode 100644 index 0000000..365e786 --- /dev/null +++ b/.changeset/fifty-radios-beam.md @@ -0,0 +1,5 @@ +--- +"saleor-app-taxes": patch +--- + +Fixed the issue when user id was not available during tax calculation. Now, to identify the user during tax calculation, we use the user email. diff --git a/apps/taxes/graphql/fragments/CalculateTaxesEvent.graphql b/apps/taxes/graphql/fragments/CalculateTaxesEvent.graphql index 0273ede..08c11e0 100644 --- a/apps/taxes/graphql/fragments/CalculateTaxesEvent.graphql +++ b/apps/taxes/graphql/fragments/CalculateTaxesEvent.graphql @@ -10,11 +10,5 @@ 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 0a0efbf..4728d97 100644 --- a/apps/taxes/graphql/fragments/TaxBase.graphql +++ b/apps/taxes/graphql/fragments/TaxBase.graphql @@ -60,11 +60,14 @@ fragment TaxBase on TaxableObject { ...TaxBaseLine } sourceObject { + __typename ... on Checkout { avataxEntityCode: metafield(key: "avataxEntityCode") + email } ... on Order { avataxEntityCode: metafield(key: "avataxEntityCode") + userEmail } } } diff --git a/apps/taxes/scripts/migrations/1.15-taxes-migration.ts b/apps/taxes/scripts/migrations/1.15-taxes-migration.ts new file mode 100644 index 0000000..904cbad --- /dev/null +++ b/apps/taxes/scripts/migrations/1.15-taxes-migration.ts @@ -0,0 +1,15 @@ +/* eslint-disable multiline-comment-style */ +import { checkoutCalculateTaxesSyncWebhook } from "../../src/pages/api/webhooks/checkout-calculate-taxes"; +import { orderCalculateTaxesSyncWebhook } from "../../src/pages/api/webhooks/order-calculate-taxes"; +import { AppWebhookMigrator } from "./app-webhook-migrator"; + +/** + * Contains the migration logic for the Taxes App. In the 1st step, it is expected to only write, not delete. The cleanup will be done in the 2nd step. + * @param webhookMigrator - The AppWebhookMigrator instance. + */ +export async function migrateTaxes(webhookMigrator: AppWebhookMigrator) { + // Migration plan: + // 1. Update subscriptionQuery of all calculateTaxes webhooks + webhookMigrator.updateWebhookQueryByHandler(orderCalculateTaxesSyncWebhook); + webhookMigrator.updateWebhookQueryByHandler(checkoutCalculateTaxesSyncWebhook); +} diff --git a/apps/taxes/scripts/migrations/app-webhook-migrator.ts b/apps/taxes/scripts/migrations/app-webhook-migrator.ts index cf08cba..ab268a4 100644 --- a/apps/taxes/scripts/migrations/app-webhook-migrator.ts +++ b/apps/taxes/scripts/migrations/app-webhook-migrator.ts @@ -8,6 +8,8 @@ type AppWebhookMigratorOptions = { mode: "report" | "migrate"; }; +type AppWebhookHandler = SaleorSyncWebhook | SaleorAsyncWebhook; + export class AppWebhookMigrator { private appWebhookRepository: AppWebhookRepository; private appId: string; @@ -33,7 +35,7 @@ export class AppWebhookMigrator { this.mode = mode; } - private registerWebhookFromHandler(webhookHandler: SaleorSyncWebhook | SaleorAsyncWebhook) { + private registerWebhookFromHandler(webhookHandler: AppWebhookHandler) { const manifest = webhookHandler.getWebhookManifest(this.apiUrl); if (!manifest.query) { @@ -44,15 +46,19 @@ export class AppWebhookMigrator { throw new Error("Webhook name is required"); } - return this.appWebhookRepository.create({ - appId: this.appId, - name: manifest.name, - query: manifest.query, - targetUrl: manifest.targetUrl, - asyncEvents: (manifest.asyncEvents ?? []) as WebhookEventTypeAsyncEnum[], - syncEvents: (manifest.syncEvents ?? []) as WebhookEventTypeSyncEnum[], - isActive: manifest.isActive ?? true, - }); + console.log(`⏳ Webhook ${manifest.name} will be registered`); + + if (this.mode === "migrate") { + return this.appWebhookRepository.create({ + appId: this.appId, + name: manifest.name, + query: manifest.query, + targetUrl: manifest.targetUrl, + asyncEvents: (manifest.asyncEvents ?? []) as WebhookEventTypeAsyncEnum[], + syncEvents: (manifest.syncEvents ?? []) as WebhookEventTypeSyncEnum[], + isActive: manifest.isActive ?? true, + }); + } } private async deleteWebhookById(webhookId: string) { @@ -119,6 +125,28 @@ export class AppWebhookMigrator { await this.deleteWebhookById(webhook.id); } + async updateWebhookQueryByHandler(webhookHandler: AppWebhookHandler) { + const webhooks = await this.getAppWebhooks(); + + const manifest = webhookHandler.getWebhookManifest(this.apiUrl); + const webhookName = manifest.name; + + const webhook = webhooks.find((webhook) => webhook.name === webhookName); + + if (!webhook) { + console.log(`🚧 Webhook ${webhookName} not found`); + + return; + } + + console.log(`⏳ Webhook ${webhookName} query will be updated`); + + if (this.mode === "migrate") { + await this.appWebhookRepository.update(webhook.id, { query: manifest.query }); + console.log(`✅ Webhook ${webhookName} query updated`); + } + } + /** * Registers a webhook if it doesn't exist based on a handler. * @param webhookHandler - The handler of the webhook we want to register. diff --git a/apps/taxes/scripts/migrations/app-webhook-repository.ts b/apps/taxes/scripts/migrations/app-webhook-repository.ts index 4cb1126..2a0683e 100644 --- a/apps/taxes/scripts/migrations/app-webhook-repository.ts +++ b/apps/taxes/scripts/migrations/app-webhook-repository.ts @@ -14,6 +14,10 @@ import { EnableWebhookMutationVariables, FetchAppWebhooksDocument, FetchAppWebhooksQuery, + UpdateAppWebhookDocument, + UpdateAppWebhookMutation, + UpdateAppWebhookMutationVariables, + WebhookUpdateInput, } from "../../generated/graphql"; gql` @@ -55,6 +59,16 @@ gql` } `; +gql` + mutation UpdateAppWebhook($id: ID!, $input: WebhookUpdateInput!) { + webhookUpdate(id: $id, input: $input) { + webhook { + id + } + } + } +`; + gql` mutation DeleteAppWebhook($id: ID!) { webhookDelete(id: $id) { @@ -168,4 +182,21 @@ export class AppWebhookRepository { return data?.webhookDelete?.webhook?.id; } + + async update(id: string, input: WebhookUpdateInput) { + const { error, data } = await this.client + .mutation(UpdateAppWebhookDocument, { + id, + input, + } as UpdateAppWebhookMutationVariables) + .toPromise(); + + if (error) { + console.log(`❌ Was not able to update webhook ${id}`, error.message); + + throw error; + } + + return data?.webhookUpdate?.webhook?.id; + } } diff --git a/apps/taxes/scripts/migrations/run-migration.ts b/apps/taxes/scripts/migrations/run-migration.ts index ed527e3..87dbd56 100644 --- a/apps/taxes/scripts/migrations/run-migration.ts +++ b/apps/taxes/scripts/migrations/run-migration.ts @@ -3,7 +3,7 @@ import * as dotenv from "dotenv"; import { createAppWebhookMigrator } from "./app-webhook-migrator"; import { fetchCloudAplEnvs, verifyRequiredEnvs } from "./migration-utils"; -import { migrateTaxes } from "./1.13-taxes-migration"; +import { migrateTaxes } from "./1.15-taxes-migration"; dotenv.config(); diff --git a/apps/taxes/scripts/migrations/run-report.ts b/apps/taxes/scripts/migrations/run-report.ts index c481964..30058b7 100644 --- a/apps/taxes/scripts/migrations/run-report.ts +++ b/apps/taxes/scripts/migrations/run-report.ts @@ -3,7 +3,7 @@ import * as dotenv from "dotenv"; import { createAppWebhookMigrator } from "./app-webhook-migrator"; import { fetchCloudAplEnvs, verifyRequiredEnvs } from "./migration-utils"; -import { migrateTaxes } from "./1.13-taxes-migration"; +import { migrateTaxes } from "./1.15-taxes-migration"; dotenv.config(); 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 67955a4..107115b 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,6 +107,8 @@ const defaultTaxBase: TaxBase = { ], sourceObject: { avataxEntityCode: null, + __typename: "Checkout", + email: "test@saleor.io", }, }; 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 1612af1..2725c25 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 @@ -66,8 +66,10 @@ describe("AvataxCalculateTaxesPayloadTransformer", () => { expect(payload.model.discount).toEqual(0); }); - it("when no issuingPrincipal.id, throws an error", async () => { - const taxBaseMock = mockGenerator.generateTaxBase(); + it("when no email in sourceObject, throws an error", async () => { + const taxBaseMock = mockGenerator.generateTaxBase({ + sourceObject: { email: undefined, __typename: "Checkout" }, + }); const matchesMock = mockGenerator.generateTaxCodeMatches(); const payloadMock = { 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 1eb5e99..e3cf91e 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 @@ -21,6 +21,19 @@ export class AvataxCalculateTaxesPayloadTransformer { return DocumentType.SalesOrder; } + // During the checkout process, it appears the customer id is not always available. We can use the email address instead. + private resolveCustomerCode(payload: CalculateTaxesPayload): string { + if (payload.taxBase.sourceObject.__typename === "Checkout") { + return taxProviderUtils.resolveStringOrThrow(payload.taxBase.sourceObject.email); + } + + if (payload.taxBase.sourceObject.__typename === "Order") { + return taxProviderUtils.resolveStringOrThrow(payload.taxBase.sourceObject.userEmail); + } + + throw new Error("Cannot resolve customer code"); + } + async transform( payload: CalculateTaxesPayload, avataxConfig: AvataxConfig, @@ -33,9 +46,7 @@ export class AvataxCalculateTaxesPayloadTransformer { payload.taxBase.sourceObject.avataxEntityCode, ); - const customerCode = taxProviderUtils.resolveStringOrThrow( - payload.issuingPrincipal?.__typename === "User" ? payload.issuingPrincipal.id : undefined, - ); + const customerCode = this.resolveCustomerCode(payload); return { model: { 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 8d80ffd..26ecdcf 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,6 +96,8 @@ const taxIncludedTaxBase: TaxBase = { ], sourceObject: { avataxEntityCode: null, + __typename: "Checkout", + email: "test@saleor.io", }, }; @@ -191,6 +193,8 @@ const taxExcludedTaxBase: TaxBase = { ], sourceObject: { avataxEntityCode: null, + __typename: "Checkout", + email: "test@saleor.io", }, };