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
}
}
issuingPrincipal {
__typename
... on User {
id
}
}
}
}

View file

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

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";
};
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.

View file

@ -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<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 { 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();

View file

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

View file

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

View file

@ -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 = {

View file

@ -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: {

View file

@ -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",
},
};