fix checkout calculate taxes (#919)

* fix: 🐛 value of customerCode in calculateTaxes

* build: 👷 add changeset

* fix: 🐛 tests
This commit is contained in:
Adrian Pilarczyk 2023-08-25 11:19:53 +02:00 committed by GitHub
parent 4a635620c4
commit 45ed9fb444
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 114 additions and 63 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -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<AvataxCalculateTaxesPayload, AvataxCalculateTaxesResponse>
implements WebhookAdapter<CalculateTaxesPayload, AvataxCalculateTaxesResponse>
{
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<AvataxCalculateTaxesResponse> {
this.logger.debug("Transforming the Saleor payload for calculating taxes with AvaTax...");
async send(payload: CalculateTaxesPayload): Promise<AvataxCalculateTaxesResponse> {
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...");

View file

@ -107,9 +107,6 @@ const defaultTaxBase: TaxBase = {
],
sourceObject: {
avataxEntityCode: null,
user: {
id: "VXNlcjoyMDg0NTEwNDEw",
},
},
};

View file

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

View file

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

View file

@ -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<CreateTransactionArgs> {
const matches = await this.getMatches();
const payloadTransformer = new AvataxCalculateTaxesPayloadTransformer();
return payloadTransformer.transform(taxBase, avataxConfig, matches);
return payloadTransformer.transform(payload, avataxConfig, matches);
}
}

View file

@ -25,7 +25,7 @@ export class AvataxOrderConfirmedPayloadTransformer {
async transform(
order: OrderConfirmedSubscriptionFragment,
avataxConfig: AvataxConfig,
matches: AvataxTaxCodeMatches
matches: AvataxTaxCodeMatches,
): Promise<CreateTransactionArgs> {
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),
),
},
};

View file

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

View file

@ -16,7 +16,10 @@ function resolveOptionalOrThrow<T>(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 = {

View file

@ -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<CalculateTaxesResponse>;
calculateTaxes: (payload: CalculateTaxesPayload) => Promise<CalculateTaxesResponse>;
confirmOrder: (payload: OrderConfirmedSubscriptionFragment) => Promise<CreateOrderResponse>;
cancelOrder: (payload: OrderCancelledPayload) => Promise<void>;
}

View file

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

View file

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

View file

@ -14,7 +14,10 @@ export const config = {
},
};
type CalculateTaxesPayload = Extract<CalculateTaxesEventFragment, { __typename: "CalculateTaxes" }>;
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));

View file

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