fix customer code calculate taxes (#922)

* fix: 🐛 calculate taxes customerCode

* build: 👷 changeset

* refactor: ♻️ address feedback

* feat:  add migration

* Empty-Commit
This commit is contained in:
Adrian Pilarczyk 2023-08-25 14:50:47 +02:00 committed by GitHub
parent 6f1c5c9436
commit 34efd39dcf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 118 additions and 23 deletions

View file

@ -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.

View file

@ -10,11 +10,5 @@ fragment CalculateTaxesEvent on Event {
value value
} }
} }
issuingPrincipal {
__typename
... on User {
id
}
}
} }
} }

View file

@ -60,11 +60,14 @@ fragment TaxBase on TaxableObject {
...TaxBaseLine ...TaxBaseLine
} }
sourceObject { sourceObject {
__typename
... on Checkout { ... on Checkout {
avataxEntityCode: metafield(key: "avataxEntityCode") avataxEntityCode: metafield(key: "avataxEntityCode")
email
} }
... on Order { ... on Order {
avataxEntityCode: metafield(key: "avataxEntityCode") avataxEntityCode: metafield(key: "avataxEntityCode")
userEmail
} }
} }
} }

View file

@ -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);
}

View file

@ -8,6 +8,8 @@ type AppWebhookMigratorOptions = {
mode: "report" | "migrate"; mode: "report" | "migrate";
}; };
type AppWebhookHandler = SaleorSyncWebhook | SaleorAsyncWebhook;
export class AppWebhookMigrator { export class AppWebhookMigrator {
private appWebhookRepository: AppWebhookRepository; private appWebhookRepository: AppWebhookRepository;
private appId: string; private appId: string;
@ -33,7 +35,7 @@ export class AppWebhookMigrator {
this.mode = mode; this.mode = mode;
} }
private registerWebhookFromHandler(webhookHandler: SaleorSyncWebhook | SaleorAsyncWebhook) { private registerWebhookFromHandler(webhookHandler: AppWebhookHandler) {
const manifest = webhookHandler.getWebhookManifest(this.apiUrl); const manifest = webhookHandler.getWebhookManifest(this.apiUrl);
if (!manifest.query) { if (!manifest.query) {
@ -44,15 +46,19 @@ export class AppWebhookMigrator {
throw new Error("Webhook name is required"); throw new Error("Webhook name is required");
} }
return this.appWebhookRepository.create({ console.log(`⏳ Webhook ${manifest.name} will be registered`);
appId: this.appId,
name: manifest.name, if (this.mode === "migrate") {
query: manifest.query, return this.appWebhookRepository.create({
targetUrl: manifest.targetUrl, appId: this.appId,
asyncEvents: (manifest.asyncEvents ?? []) as WebhookEventTypeAsyncEnum[], name: manifest.name,
syncEvents: (manifest.syncEvents ?? []) as WebhookEventTypeSyncEnum[], query: manifest.query,
isActive: manifest.isActive ?? true, targetUrl: manifest.targetUrl,
}); asyncEvents: (manifest.asyncEvents ?? []) as WebhookEventTypeAsyncEnum[],
syncEvents: (manifest.syncEvents ?? []) as WebhookEventTypeSyncEnum[],
isActive: manifest.isActive ?? true,
});
}
} }
private async deleteWebhookById(webhookId: string) { private async deleteWebhookById(webhookId: string) {
@ -119,6 +125,28 @@ export class AppWebhookMigrator {
await this.deleteWebhookById(webhook.id); 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. * Registers a webhook if it doesn't exist based on a handler.
* @param webhookHandler - The handler of the webhook we want to register. * @param webhookHandler - The handler of the webhook we want to register.

View file

@ -14,6 +14,10 @@ import {
EnableWebhookMutationVariables, EnableWebhookMutationVariables,
FetchAppWebhooksDocument, FetchAppWebhooksDocument,
FetchAppWebhooksQuery, FetchAppWebhooksQuery,
UpdateAppWebhookDocument,
UpdateAppWebhookMutation,
UpdateAppWebhookMutationVariables,
WebhookUpdateInput,
} from "../../generated/graphql"; } from "../../generated/graphql";
gql` gql`
@ -55,6 +59,16 @@ gql`
} }
`; `;
gql`
mutation UpdateAppWebhook($id: ID!, $input: WebhookUpdateInput!) {
webhookUpdate(id: $id, input: $input) {
webhook {
id
}
}
}
`;
gql` gql`
mutation DeleteAppWebhook($id: ID!) { mutation DeleteAppWebhook($id: ID!) {
webhookDelete(id: $id) { webhookDelete(id: $id) {
@ -168,4 +182,21 @@ export class AppWebhookRepository {
return data?.webhookDelete?.webhook?.id; return data?.webhookDelete?.webhook?.id;
} }
async update(id: string, input: WebhookUpdateInput) {
const { error, data } = await this.client
.mutation<UpdateAppWebhookMutation>(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;
}
} }

View file

@ -3,7 +3,7 @@
import * as dotenv from "dotenv"; import * as dotenv from "dotenv";
import { createAppWebhookMigrator } from "./app-webhook-migrator"; import { createAppWebhookMigrator } from "./app-webhook-migrator";
import { fetchCloudAplEnvs, verifyRequiredEnvs } from "./migration-utils"; import { fetchCloudAplEnvs, verifyRequiredEnvs } from "./migration-utils";
import { migrateTaxes } from "./1.13-taxes-migration"; import { migrateTaxes } from "./1.15-taxes-migration";
dotenv.config(); dotenv.config();

View file

@ -3,7 +3,7 @@
import * as dotenv from "dotenv"; import * as dotenv from "dotenv";
import { createAppWebhookMigrator } from "./app-webhook-migrator"; import { createAppWebhookMigrator } from "./app-webhook-migrator";
import { fetchCloudAplEnvs, verifyRequiredEnvs } from "./migration-utils"; import { fetchCloudAplEnvs, verifyRequiredEnvs } from "./migration-utils";
import { migrateTaxes } from "./1.13-taxes-migration"; import { migrateTaxes } from "./1.15-taxes-migration";
dotenv.config(); dotenv.config();

View file

@ -107,6 +107,8 @@ const defaultTaxBase: TaxBase = {
], ],
sourceObject: { sourceObject: {
avataxEntityCode: null, avataxEntityCode: null,
__typename: "Checkout",
email: "test@saleor.io",
}, },
}; };

View file

@ -66,8 +66,10 @@ describe("AvataxCalculateTaxesPayloadTransformer", () => {
expect(payload.model.discount).toEqual(0); expect(payload.model.discount).toEqual(0);
}); });
it("when no issuingPrincipal.id, throws an error", async () => { it("when no email in sourceObject, throws an error", async () => {
const taxBaseMock = mockGenerator.generateTaxBase(); const taxBaseMock = mockGenerator.generateTaxBase({
sourceObject: { email: undefined, __typename: "Checkout" },
});
const matchesMock = mockGenerator.generateTaxCodeMatches(); const matchesMock = mockGenerator.generateTaxCodeMatches();
const payloadMock = { const payloadMock = {

View file

@ -21,6 +21,19 @@ export class AvataxCalculateTaxesPayloadTransformer {
return DocumentType.SalesOrder; 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( async transform(
payload: CalculateTaxesPayload, payload: CalculateTaxesPayload,
avataxConfig: AvataxConfig, avataxConfig: AvataxConfig,
@ -33,9 +46,7 @@ export class AvataxCalculateTaxesPayloadTransformer {
payload.taxBase.sourceObject.avataxEntityCode, payload.taxBase.sourceObject.avataxEntityCode,
); );
const customerCode = taxProviderUtils.resolveStringOrThrow( const customerCode = this.resolveCustomerCode(payload);
payload.issuingPrincipal?.__typename === "User" ? payload.issuingPrincipal.id : undefined,
);
return { return {
model: { model: {

View file

@ -96,6 +96,8 @@ const taxIncludedTaxBase: TaxBase = {
], ],
sourceObject: { sourceObject: {
avataxEntityCode: null, avataxEntityCode: null,
__typename: "Checkout",
email: "test@saleor.io",
}, },
}; };
@ -191,6 +193,8 @@ const taxExcludedTaxBase: TaxBase = {
], ],
sourceObject: { sourceObject: {
avataxEntityCode: null, avataxEntityCode: null,
__typename: "Checkout",
email: "test@saleor.io",
}, },
}; };